Merge pull request #1 from efroemling/master

update to latest build
This commit is contained in:
Daniil Rakhov 2021-04-19 08:51:31 +03:00 committed by GitHub
commit a6fb047189
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
966 changed files with 210385 additions and 45848 deletions

38
.editorconfig Normal file
View File

@ -0,0 +1,38 @@
# THIS FILE WAS AUTOGENERATED; DO NOT EDIT.
# Source: config/toolconfigsrc/editorconfig.
# Common config file for supported editors; see editorconfig.org.
# If you are using an editor that doesn't support editorconfig, try to
# conform to these settings manually.
# Note: while we autogenerate this file, we also check it into git
# because it affects how source is displayed on github.
# This is the top-most EditorConfig file.
root = true
# Defaults for all files.
[*]
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
# Python overrides.
[*.py]
indent_style = space
indent_size = 4
max_line_length = 79
charset = utf-8
# Makefile overrides.
[Makefile]
indent_style = tab
indent_size = 2
max_line_length = 80
# C/C++ overrides.
[*.{c,cc,h,cpp,hpp}]
indent_style = space
indent_size = 2
max_line_length = 80

File diff suppressed because it is too large Load Diff

32
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,32 @@
---
name: Bug report
about: Create a report to help us improve
title: Bug
labels: bug
assignees: ''
---
## Description
Describe the bug. Do not forget to fill the title.
## Steps to reproduce
1. Launch BombSquad
2. Go to '...'
3. Press '...'
4. Bug!
## Expected behavior
Describe what you think should happen.
## Machine
**Platform**: Windows 10 / Ubuntu 20.04 LTS / AOSP 8.1 / etc.
**BombSquad version**: [1.5.27](https://github.com/efroemling/ballistica/releases/tag/v1.5.27)
**Commit**: [2642488](https://github.com/efroemling/ballistica/commit/2642488a51b250752169738f5aeeccaafa2bc8de)
Select what do you want to use: release version or commit. Please use a hyperlink.
## Screenshots
Put some screenshots here if needed.
## Extra
Put some extra information here. For example, describe your assumptions about the cause of the bug.

View File

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for BombSquad
title: Feature Request
labels: enhancement
assignees: ''
---
## Description
Describe feature that you want.
## Solution
A clear and concise description of what you want to happen.
## Alternatives
A clear and concise description of any alternative solutions or features you've considered.
## Additional context
Add any other context or screenshots about the feature request here.

View File

@ -0,0 +1,10 @@
---
name: Want to discuss something or ask a question?
about: GitHub Discussions are waiting for you
title: Discussion
labels: ''
assignees: ''
---
We're using [GitHub Discussions](https://github.com/efroemling/ballistica/discussions)! Asking and answering questions is much more convenient there than in Issues.

View File

@ -11,36 +11,49 @@ on:
jobs:
# We run most of our testing on linux but it should apply to mac too;
# we can always add an explicit mac job if it seems worthwhile.
ci_unix:
runs-on: ubuntu-18.04
# We run most of our testing only on linux but it should apply to mac too;
# we can always add an explicit mac job later if it seems worthwhile.
check_linux:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v1
- name: Set up Python
uses: actions/setup-python@v1
with:
python-version: 3.7
python-version: 3.8
- name: Install dependencies
run: tools/snippets install_pip_reqs
run: tools/pcommand install_pip_reqs
- name: Run checks and tests
run: make -j2 check test
# Compile just a server binary but don't run asset builds/etc.
# (to spare my asset file server)
compile_linux:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v1
- name: Set up Python
uses: actions/setup-python@v1
with:
python-version: 3.8
- name: Compile binary
run: make _cmake-simple-ci-server-build
# Most of our toolset doesn't work on raw windows (outside of WSL).
# However, it's nice to at least run unit tests there since some behavior
# (filesystem, etc) can vary significantly.
ci_windows:
# (filesystem, etc) can vary significantly between windows and linux/apple.
check_windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v1
- name: Set up Python
uses: actions/setup-python@v1
with:
python-version: 3.7
python-version: 3.8
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest
pip install pytest typing_extensions
- name: Run tests
run: python tools/snippets pytest -v tests
run: python tools/pcommand pytest -v tests

3
.gitignore vendored
View File

@ -23,7 +23,6 @@ local.properties
.flycheck-dir-locals.el
.pylintrc
.projectile
.editorconfig
.clang-format
.style.yapf
.irony
@ -74,7 +73,9 @@ libs/
!vc_redist.x86.exe
!vc_redist.x64.exe
!python.exe
!python_d.exe
!pythonw.exe
!pythonw_d.exe
!**/OculusSDK/Tools/**/*.exe
# Note: specifying exact Debug/Release dirs for now; we wind up ignoring

View File

@ -62,8 +62,9 @@
<excludePattern pattern=".asset_manifest_*.json" />
<excludePattern pattern=".efrocachemap" />
<excludePattern pattern=".pytest_cache" />
<excludePattern pattern=".editorconfig" />
</content>
<orderEntry type="jdk" jdkName="Python 3.7" jdkType="Python SDK" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

File diff suppressed because it is too large Load Diff

8
.idea/dictionaries/roman.xml generated Normal file
View File

@ -0,0 +1,8 @@
<component name="ProjectDictionaryState">
<dictionary name="roman">
<words>
<w>maxlen</w>
<w>pagename</w>
</words>
</dictionary>
</component>

View File

@ -2,6 +2,8 @@
<profile version="1.0">
<option name="myName" value="Default" />
<inspection_tool class="DuplicatedCode" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="GrazieInspection" enabled="false" level="TYPO" enabled_by_default="false" />
<inspection_tool class="HttpUrlsUsage" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="InconsistentLineSeparators" enabled="true" level="WARNING" enabled_by_default="true">
<scope name="NonPython" level="WARNING" enabled="false" />
</inspection_tool>
@ -42,6 +44,7 @@
</inspection_tool>
<inspection_tool class="PyRedundantParenthesesInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<scope name="UncheckedPython" level="WEAK WARNING" enabled="false" />
<option name="myIgnoreTupleInReturn" value="true" />
</inspection_tool>
<inspection_tool class="PyShadowingBuiltinsInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<scope name="UncheckedPython" level="WEAK WARNING" enabled="false" />
@ -50,8 +53,9 @@
<scope name="UncheckedPython" level="WEAK WARNING" enabled="false" />
</inspection_tool>
<inspection_tool class="PyTypeCheckerInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="PyTypeHintsInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="PyUnreachableCodeInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="false">
<scope name="PyIgnoreUnresolved" level="WARNING" enabled="false">
<option name="ignoredIdentifiers">
<list>

2
.idea/misc.xml generated
View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.7" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8" project-jdk-type="Python SDK" />
<component name="PythonCompatibilityInspectionAdvertiser">
<option name="version" value="3" />
</component>

View File

@ -1,12 +1,197 @@
### 1.6.0 (20333)
- 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)
- The meta subsystem now enables new plugins by default in headless builds.
- Added option to save party in Manual tab
- Slight tidying on the tourney entry popup
- Env var to override UI scale is now `BA_UI_SCALE` instead of `BA_FORCE_UI_SCALE`.
- Fixed an issue where ba.storagename() could prevent objects on the stack from getting released cleanly
- Improvements to documentation generation such as link to some external base types.
- Added `ba.clipboard_*` functions for copying and pasting text on supported platforms.
- Implemented clipboard functionality on SDL based builds (such as prefab).
- Fixed an issue where click locations on scaled text fields could be incorrectly calculated.
- 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.
- 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
### 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.
### 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.
### 1.5.27 (20238)
- 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
- Ditto with AccountSubsystem 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.
- 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
- Added android input-device detection log messages 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)
### 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.
- 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.
- 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).
### 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.
### 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`.
### 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 really bad 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)
- 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)
- Other minor bug fixes.
### 1.5.7 (20077)
- 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.
- 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.
- 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 install 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.
- 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.
- 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 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.
- 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 its 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)
- '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
@ -16,7 +201,7 @@
- Various other minor name changes (bs.getUIBounds() -> ba.app.uibounds, 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.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 rename.
@ -24,9 +209,9 @@
- 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()
- 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.
- 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.
@ -36,19 +221,22 @@
- 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()
- 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'.
- '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.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..
- 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.
@ -56,8 +244,8 @@
### 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).
- 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
@ -95,7 +283,7 @@
- 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 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
@ -215,10 +403,10 @@
- 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
- 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
- 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
### 1.4.94

View File

@ -9,9 +9,14 @@
- Fixed some game modes
### Roman Trapeznikov
###### Ballistica
- Bug fixes
###### beyond the project code
- Game servers developer
- Modder
- Bug fixes and code cleanup
### Benefit-Zebra
- Unofficial BombSquad Bug Finder
- Code Cleanup
### Ali Borhani
- Bug fixes
### Mr.Smoothy
- Modder

View File

@ -1,4 +1,6 @@
Copyright (c) 2011-2019 Eric Froemling
Note: portions of this software, namely external libraries, are covered by
other licenses such as BSD. See individual licenses under src/external/*.
All other code is covered by the following (MIT License):
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

920
Makefile

File diff suppressed because it is too large Load Diff

View File

@ -24,10 +24,10 @@ The Ballistica project is the foundation for the next generation of [BombSquad](
### Frequently Asked Questions
* **Q: What's with this new name? Is BombSquad getting renamed?**
* A: No, BombSquad is still BombSquad. 'Ballistica' is simply the new name for the engine/app-framework. This way it can also be used for other game/app projects without causing confusion (though that is mostly theoretical at this point). As a modder, the biggest changes you will notice is 'ba' prefixes in the API instead of 'bs' and naming that follows Python PEP8 standards (underscores and lowercase instead of camel-case). So `bs.playSound(mySound)` in the old system might look like `ba.playsound(mysound)` in the new. You may also see the word 'BallisticaCore' show up various places, which in actual releases gets replaced by 'BombSquad'.
* A: No, BombSquad is still BombSquad. 'Ballistica' is simply the new name for the engine/app-framework. This way it can also be used for other game/app projects without causing confusion (though that is mostly theoretical at this point). As a modder, the biggest changes you will notice is 'ba' prefixes in the API instead of 'bs' and naming that follows Python PEP8 standards (underscores and lowercase instead of camel-case). So `bs.playSound(mySound)` in the old system might look like `ba.playsound(my_sound)` in the new. You may also see the word 'BallisticaCore' show up various places, which in actual releases gets replaced by 'BombSquad'.
* **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, etc. Over time I hope to expand this to include at least some 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 game binaries. So in a nutshell: create and share mods to your heart's content, but please don't distribute your own complete copies of the game 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 game libraries/binaries. So in a nutshell: create and share mods to your heart's content, but please don't distribute your own complete copies of the game without permission. Please email support@froemling.net if you have any questions about this.
* **Q: Will my existing BombSquad 1.4.x mods still work?**
* A: No. All mods will need to be explicitly updated to work with the new ballistica apis in 1.5+. This may or may not be a significant amount of work depending on the mod. I would highly suggest tinkering around with some of the new features in 1.5 such as type-safe Python and dynamic assets before attempting to port any old mods, as some things are done significantly differently now. You may also want to consider simply sticking with 1.4 builds for a while longer, especially for server duties, since they will remain fully compatible with clients running 1.5. The new ballistica APIs may be changing significantly for at least a while as the dust settles, but they will be worth switching to in the end, I promise!
* A: Not 'out of the box'. All mods will need to be explicitly updated to work with the new ballistica apis in 1.5+. This may or may not be a significant amount of work depending on the mod. I would highly suggest tinkering around with some of the new features in 1.5 such as type-safe Python and dynamic assets before attempting to port any old mods, as some things are done significantly differently now. You may also want to consider simply sticking with 1.4 builds for a while longer, especially for server duties, since they will remain fully compatible with clients running 1.5. The new ballistica APIs may be changing significantly for at least a while as the dust settles, but they will be worth switching to in the end, I promise!

File diff suppressed because it is too large Load Diff

View File

@ -1,70 +1,84 @@
[
"ba_data/python/ba/__init__.py",
"ba_data/python/ba/__pycache__/__init__.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_account.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_achievement.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_activity.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_activitytypes.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_actor.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_app.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_appconfig.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_appdelegate.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_apputils.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_assetmanager.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_benchmark.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_campaign.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_coopgame.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_coopsession.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_dependency.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_dualteamsession.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_enums.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_error.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_freeforallsession.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_gameactivity.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_gameresults.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_gameutils.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_general.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_hooks.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_input.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_lang.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_level.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_lobby.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_map.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_math.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_messages.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_meta.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_modutils.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_multiteamsession.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_music.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_netutils.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_nodeactor.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_playlist.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_powerup.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_profile.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_servermode.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_session.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_stats.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_store.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_team.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_teamgame.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_tips.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_tournament.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/deprecated.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/internal.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/macmusicapp.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/osmusic.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/__init__.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_account.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_achievement.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_activity.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_activitytypes.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_actor.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_ads.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_analytics.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_app.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_appconfig.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_appdelegate.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_appmode.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_apputils.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_assetmanager.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_benchmark.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_campaign.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_collision.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_coopgame.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_coopsession.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_dependency.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_dualteamsession.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_enums.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_error.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_freeforallsession.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_gameactivity.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_gameresults.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_gameutils.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_general.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_hooks.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_input.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_keyboard.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_language.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_level.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_lobby.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_map.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_math.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_messages.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_meta.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_multiteamsession.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_music.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_netutils.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_nodeactor.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_player.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_playlist.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_plugin.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_powerup.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_profile.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_score.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_servermode.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_session.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_settings.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_stats.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_store.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_team.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_teamgame.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_tips.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_tournament.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/_ui.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/deprecated.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/internal.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/macmusicapp.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/modutils.cpython-38.opt-1.pyc",
"ba_data/python/ba/__pycache__/osmusic.cpython-38.opt-1.pyc",
"ba_data/python/ba/_account.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/_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/_benchmark.py",
"ba_data/python/ba/_campaign.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",
@ -78,60 +92,69 @@
"ba_data/python/ba/_general.py",
"ba_data/python/ba/_hooks.py",
"ba_data/python/ba/_input.py",
"ba_data/python/ba/_lang.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/_map.py",
"ba_data/python/ba/_math.py",
"ba_data/python/ba/_messages.py",
"ba_data/python/ba/_meta.py",
"ba_data/python/ba/_modutils.py",
"ba_data/python/ba/_multiteamsession.py",
"ba_data/python/ba/_music.py",
"ba_data/python/ba/_netutils.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/deprecated.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-37.opt-1.pyc",
"ba_data/python/ba/ui/__pycache__/__init__.cpython-38.opt-1.pyc",
"ba_data/python/bacommon/__init__.py",
"ba_data/python/bacommon/__pycache__/__init__.cpython-37.opt-1.pyc",
"ba_data/python/bacommon/__pycache__/assets.cpython-37.opt-1.pyc",
"ba_data/python/bacommon/__pycache__/err.cpython-37.opt-1.pyc",
"ba_data/python/bacommon/__pycache__/servermanager.cpython-37.opt-1.pyc",
"ba_data/python/bacommon/__pycache__/__init__.cpython-38.opt-1.pyc",
"ba_data/python/bacommon/__pycache__/assets.cpython-38.opt-1.pyc",
"ba_data/python/bacommon/__pycache__/err.cpython-38.opt-1.pyc",
"ba_data/python/bacommon/__pycache__/net.cpython-38.opt-1.pyc",
"ba_data/python/bacommon/__pycache__/servermanager.cpython-38.opt-1.pyc",
"ba_data/python/bacommon/assets.py",
"ba_data/python/bacommon/err.py",
"ba_data/python/bacommon/net.py",
"ba_data/python/bacommon/servermanager.py",
"ba_data/python/bastd/__init__.py",
"ba_data/python/bastd/__pycache__/__init__.cpython-37.opt-1.pyc",
"ba_data/python/bastd/__pycache__/appdelegate.cpython-37.opt-1.pyc",
"ba_data/python/bastd/__pycache__/mainmenu.cpython-37.opt-1.pyc",
"ba_data/python/bastd/__pycache__/maps.cpython-37.opt-1.pyc",
"ba_data/python/bastd/__pycache__/stdmap.cpython-37.opt-1.pyc",
"ba_data/python/bastd/__pycache__/tutorial.cpython-37.opt-1.pyc",
"ba_data/python/bastd/__pycache__/__init__.cpython-38.opt-1.pyc",
"ba_data/python/bastd/__pycache__/appdelegate.cpython-38.opt-1.pyc",
"ba_data/python/bastd/__pycache__/gameutils.cpython-38.opt-1.pyc",
"ba_data/python/bastd/__pycache__/mainmenu.cpython-38.opt-1.pyc",
"ba_data/python/bastd/__pycache__/maps.cpython-38.opt-1.pyc",
"ba_data/python/bastd/__pycache__/stdmap.cpython-38.opt-1.pyc",
"ba_data/python/bastd/__pycache__/tutorial.cpython-38.opt-1.pyc",
"ba_data/python/bastd/activity/__init__.py",
"ba_data/python/bastd/activity/__pycache__/__init__.cpython-37.opt-1.pyc",
"ba_data/python/bastd/activity/__pycache__/coopjoin.cpython-37.opt-1.pyc",
"ba_data/python/bastd/activity/__pycache__/coopscore.cpython-37.opt-1.pyc",
"ba_data/python/bastd/activity/__pycache__/drawscore.cpython-37.opt-1.pyc",
"ba_data/python/bastd/activity/__pycache__/dualteamscore.cpython-37.opt-1.pyc",
"ba_data/python/bastd/activity/__pycache__/freeforallvictory.cpython-37.opt-1.pyc",
"ba_data/python/bastd/activity/__pycache__/multiteamjoin.cpython-37.opt-1.pyc",
"ba_data/python/bastd/activity/__pycache__/multiteamscore.cpython-37.opt-1.pyc",
"ba_data/python/bastd/activity/__pycache__/multiteamvictory.cpython-37.opt-1.pyc",
"ba_data/python/bastd/activity/__pycache__/__init__.cpython-38.opt-1.pyc",
"ba_data/python/bastd/activity/__pycache__/coopjoin.cpython-38.opt-1.pyc",
"ba_data/python/bastd/activity/__pycache__/coopscore.cpython-38.opt-1.pyc",
"ba_data/python/bastd/activity/__pycache__/drawscore.cpython-38.opt-1.pyc",
"ba_data/python/bastd/activity/__pycache__/dualteamscore.cpython-38.opt-1.pyc",
"ba_data/python/bastd/activity/__pycache__/freeforallvictory.cpython-38.opt-1.pyc",
"ba_data/python/bastd/activity/__pycache__/multiteamjoin.cpython-38.opt-1.pyc",
"ba_data/python/bastd/activity/__pycache__/multiteamscore.cpython-38.opt-1.pyc",
"ba_data/python/bastd/activity/__pycache__/multiteamvictory.cpython-38.opt-1.pyc",
"ba_data/python/bastd/activity/coopjoin.py",
"ba_data/python/bastd/activity/coopscore.py",
"ba_data/python/bastd/activity/drawscore.py",
@ -141,27 +164,27 @@
"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-37.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/background.cpython-37.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/bomb.cpython-37.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/controlsguide.cpython-37.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/flag.cpython-37.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/image.cpython-37.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/onscreencountdown.cpython-37.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/onscreentimer.cpython-37.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/playerspaz.cpython-37.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/popuptext.cpython-37.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/powerupbox.cpython-37.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/respawnicon.cpython-37.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/scoreboard.cpython-37.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/spawner.cpython-37.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/spaz.cpython-37.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/spazappearance.cpython-37.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/spazbot.cpython-37.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/spazfactory.cpython-37.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/text.cpython-37.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/tipstext.cpython-37.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/zoomtext.cpython-37.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/__init__.cpython-38.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/background.cpython-38.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/bomb.cpython-38.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/controlsguide.cpython-38.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/flag.cpython-38.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/image.cpython-38.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/onscreencountdown.cpython-38.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/onscreentimer.cpython-38.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/playerspaz.cpython-38.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/popuptext.cpython-38.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/powerupbox.cpython-38.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/respawnicon.cpython-38.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/scoreboard.cpython-38.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/spawner.cpython-38.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/spaz.cpython-38.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/spazappearance.cpython-38.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/spazbot.cpython-38.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/spazfactory.cpython-38.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/text.cpython-38.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/tipstext.cpython-38.opt-1.pyc",
"ba_data/python/bastd/actor/__pycache__/zoomtext.cpython-38.opt-1.pyc",
"ba_data/python/bastd/actor/background.py",
"ba_data/python/bastd/actor/bomb.py",
"ba_data/python/bastd/actor/controlsguide.py",
@ -184,25 +207,25 @@
"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-37.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/assault.cpython-37.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/capturetheflag.cpython-37.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/chosenone.cpython-37.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/conquest.cpython-37.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/deathmatch.cpython-37.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/easteregghunt.cpython-37.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/elimination.cpython-37.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/football.cpython-37.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/hockey.cpython-37.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/keepaway.cpython-37.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/kingofthehill.cpython-37.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/meteorshower.cpython-37.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/ninjafight.cpython-37.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/onslaught.cpython-37.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/race.cpython-37.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/runaround.cpython-37.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/targetpractice.cpython-37.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/thelaststand.cpython-37.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/__init__.cpython-38.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/assault.cpython-38.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/capturetheflag.cpython-38.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/chosenone.cpython-38.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/conquest.cpython-38.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/deathmatch.cpython-38.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/easteregghunt.cpython-38.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/elimination.cpython-38.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/football.cpython-38.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/hockey.cpython-38.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/keepaway.cpython-38.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/kingofthehill.cpython-38.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/meteorshower.cpython-38.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/ninjafight.cpython-38.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/onslaught.cpython-38.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/race.cpython-38.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/runaround.cpython-38.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/targetpractice.cpython-38.opt-1.pyc",
"ba_data/python/bastd/game/__pycache__/thelaststand.cpython-38.opt-1.pyc",
"ba_data/python/bastd/game/assault.py",
"ba_data/python/bastd/game/capturetheflag.py",
"ba_data/python/bastd/game/chosenone.py",
@ -221,26 +244,31 @@
"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-38.opt-1.pyc",
"ba_data/python/bastd/keyboard/__pycache__/englishkeyboard.cpython-38.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-37.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/big_g.cpython-37.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/bridgit.cpython-37.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/courtyard.cpython-37.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/crag_castle.cpython-37.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/doom_shroom.cpython-37.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/football_stadium.cpython-37.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/happy_thoughts.cpython-37.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/hockey_stadium.cpython-37.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/lake_frigid.cpython-37.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/monkey_face.cpython-37.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/rampage.cpython-37.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/roundabout.cpython-37.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/step_right_up.cpython-37.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/the_pad.cpython-37.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/tip_top.cpython-37.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/tower_d.cpython-37.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/zig_zag.cpython-37.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/__init__.cpython-38.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/big_g.cpython-38.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/bridgit.cpython-38.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/courtyard.cpython-38.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/crag_castle.cpython-38.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/doom_shroom.cpython-38.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/football_stadium.cpython-38.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/happy_thoughts.cpython-38.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/hockey_stadium.cpython-38.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/lake_frigid.cpython-38.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/monkey_face.cpython-38.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/rampage.cpython-38.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/roundabout.cpython-38.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/step_right_up.cpython-38.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/the_pad.cpython-38.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/tip_top.cpython-38.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/tower_d.cpython-38.opt-1.pyc",
"ba_data/python/bastd/mapdata/__pycache__/zig_zag.cpython-38.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",
@ -260,58 +288,57 @@
"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-37.opt-1.pyc",
"ba_data/python/bastd/session/__pycache__/__init__.cpython-38.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-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/achievements.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/appinvite.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/characterpicker.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/colorpicker.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/config.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/configerror.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/confirm.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/continues.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/creditslist.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/debug.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/feedback.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/fileselector.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/gather.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/getcurrency.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/getremote.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/helpui.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/iconpicker.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/kiosk.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/mainmenu.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/onscreenkeyboard.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/party.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/partyqueue.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/play.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/playoptions.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/popup.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/promocode.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/purchase.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/qrcode.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/radiogroup.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/report.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/resourcetypeinfo.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/serverdialog.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/specialoffer.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/tabs.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/teamnamescolors.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/telnet.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/tournamententry.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/tournamentscores.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/trophies.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/url.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/watch.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/__init__.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/achievements.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/appinvite.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/characterpicker.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/colorpicker.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/config.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/configerror.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/confirm.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/continues.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/creditslist.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/debug.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/feedback.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/fileselector.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/getcurrency.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/getremote.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/helpui.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/iconpicker.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/kiosk.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/mainmenu.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/onscreenkeyboard.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/party.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/partyqueue.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/play.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/playoptions.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/popup.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/promocode.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/purchase.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/qrcode.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/radiogroup.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/report.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/resourcetypeinfo.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/serverdialog.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/specialoffer.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/tabs.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/teamnamescolors.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/telnet.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/tournamententry.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/tournamentscores.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/trophies.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/url.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/__pycache__/watch.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/account/__init__.py",
"ba_data/python/bastd/ui/account/__pycache__/__init__.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/account/__pycache__/link.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/account/__pycache__/settings.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/account/__pycache__/unlink.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/account/__pycache__/viewer.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/account/__pycache__/__init__.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/account/__pycache__/link.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/account/__pycache__/settings.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/account/__pycache__/unlink.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/account/__pycache__/viewer.cpython-38.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",
@ -325,10 +352,10 @@
"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-37.opt-1.pyc",
"ba_data/python/bastd/ui/coop/__pycache__/browser.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/coop/__pycache__/gamebutton.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/coop/__pycache__/level.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/coop/__pycache__/__init__.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/coop/__pycache__/browser.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/coop/__pycache__/gamebutton.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/coop/__pycache__/level.cpython-38.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",
@ -336,16 +363,27 @@
"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.py",
"ba_data/python/bastd/ui/gather/__init__.py",
"ba_data/python/bastd/ui/gather/__pycache__/__init__.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/gather/__pycache__/abouttab.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/gather/__pycache__/manualtab.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/gather/__pycache__/nearbytab.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/gather/__pycache__/privatetab.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/gather/__pycache__/publictab.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/gather/abouttab.py",
"ba_data/python/bastd/ui/gather/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-37.opt-1.pyc",
"ba_data/python/bastd/ui/league/__pycache__/rankbutton.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/league/__pycache__/rankwindow.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/league/__pycache__/__init__.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/league/__pycache__/rankbutton.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/league/__pycache__/rankwindow.cpython-38.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",
@ -354,15 +392,15 @@
"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-37.opt-1.pyc",
"ba_data/python/bastd/ui/playlist/__pycache__/addgame.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/playlist/__pycache__/browser.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/playlist/__pycache__/customizebrowser.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/playlist/__pycache__/edit.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/playlist/__pycache__/editcontroller.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/playlist/__pycache__/editgame.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/playlist/__pycache__/mapselect.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/playlist/__pycache__/share.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/playlist/__pycache__/__init__.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/playlist/__pycache__/addgame.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/playlist/__pycache__/browser.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/playlist/__pycache__/customizebrowser.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/playlist/__pycache__/edit.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/playlist/__pycache__/editcontroller.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/playlist/__pycache__/editgame.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/playlist/__pycache__/mapselect.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/playlist/__pycache__/share.cpython-38.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",
@ -374,10 +412,10 @@
"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-37.opt-1.pyc",
"ba_data/python/bastd/ui/profile/__pycache__/browser.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/profile/__pycache__/edit.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/profile/__pycache__/upgrade.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/profile/__pycache__/__init__.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/profile/__pycache__/browser.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/profile/__pycache__/edit.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/profile/__pycache__/upgrade.cpython-38.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",
@ -389,24 +427,25 @@
"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-37.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/advanced.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/allsettings.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/audio.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/controls.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/gamepad.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/gamepadadvanced.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/gamepadselect.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/graphics.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/keyboard.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/nettesting.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/ps3controller.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/remoteapp.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/testing.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/touchscreen.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/vrtesting.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/wiimote.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/xbox360controller.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/__init__.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/advanced.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/allsettings.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/audio.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/controls.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/gamepad.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/gamepadadvanced.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/gamepadselect.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/graphics.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/keyboard.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/nettesting.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/plugins.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/ps3controller.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/remoteapp.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/testing.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/touchscreen.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/vrtesting.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/wiimote.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/settings/__pycache__/xbox360controller.cpython-38.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",
@ -417,6 +456,7 @@
"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/ps3controller.py",
"ba_data/python/bastd/ui/settings/remoteapp.py",
"ba_data/python/bastd/ui/settings/testing.py",
@ -425,21 +465,21 @@
"ba_data/python/bastd/ui/settings/wiimote.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-37.opt-1.pyc",
"ba_data/python/bastd/ui/soundtrack/__pycache__/browser.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/soundtrack/__pycache__/edit.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/soundtrack/__pycache__/entrytypeselect.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/soundtrack/__pycache__/macmusicapp.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/soundtrack/__pycache__/__init__.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/soundtrack/__pycache__/browser.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/soundtrack/__pycache__/edit.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/soundtrack/__pycache__/entrytypeselect.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/soundtrack/__pycache__/macmusicapp.cpython-38.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-37.opt-1.pyc",
"ba_data/python/bastd/ui/store/__pycache__/browser.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/store/__pycache__/button.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/store/__pycache__/item.cpython-37.opt-1.pyc",
"ba_data/python/bastd/ui/store/__pycache__/__init__.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/store/__pycache__/browser.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/store/__pycache__/button.cpython-38.opt-1.pyc",
"ba_data/python/bastd/ui/store/__pycache__/item.cpython-38.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",
@ -452,23 +492,23 @@
"ba_data/python/bastd/ui/url.py",
"ba_data/python/bastd/ui/watch.py",
"ba_data/python/efro/__init__.py",
"ba_data/python/efro/__pycache__/__init__.cpython-37.opt-1.pyc",
"ba_data/python/efro/__pycache__/call.cpython-37.opt-1.pyc",
"ba_data/python/efro/__pycache__/dataclasses.cpython-37.opt-1.pyc",
"ba_data/python/efro/__pycache__/error.cpython-37.opt-1.pyc",
"ba_data/python/efro/__pycache__/json.cpython-37.opt-1.pyc",
"ba_data/python/efro/__pycache__/terminal.cpython-37.opt-1.pyc",
"ba_data/python/efro/__pycache__/util.cpython-37.opt-1.pyc",
"ba_data/python/efro/__pycache__/__init__.cpython-38.opt-1.pyc",
"ba_data/python/efro/__pycache__/call.cpython-38.opt-1.pyc",
"ba_data/python/efro/__pycache__/dataclasses.cpython-38.opt-1.pyc",
"ba_data/python/efro/__pycache__/error.cpython-38.opt-1.pyc",
"ba_data/python/efro/__pycache__/json.cpython-38.opt-1.pyc",
"ba_data/python/efro/__pycache__/terminal.cpython-38.opt-1.pyc",
"ba_data/python/efro/__pycache__/util.cpython-38.opt-1.pyc",
"ba_data/python/efro/call.py",
"ba_data/python/efro/dataclasses.py",
"ba_data/python/efro/entity/__init__.py",
"ba_data/python/efro/entity/__pycache__/__init__.cpython-37.opt-1.pyc",
"ba_data/python/efro/entity/__pycache__/_base.cpython-37.opt-1.pyc",
"ba_data/python/efro/entity/__pycache__/_entity.cpython-37.opt-1.pyc",
"ba_data/python/efro/entity/__pycache__/_field.cpython-37.opt-1.pyc",
"ba_data/python/efro/entity/__pycache__/_support.cpython-37.opt-1.pyc",
"ba_data/python/efro/entity/__pycache__/_value.cpython-37.opt-1.pyc",
"ba_data/python/efro/entity/__pycache__/util.cpython-37.opt-1.pyc",
"ba_data/python/efro/entity/__pycache__/__init__.cpython-38.opt-1.pyc",
"ba_data/python/efro/entity/__pycache__/_base.cpython-38.opt-1.pyc",
"ba_data/python/efro/entity/__pycache__/_entity.cpython-38.opt-1.pyc",
"ba_data/python/efro/entity/__pycache__/_field.cpython-38.opt-1.pyc",
"ba_data/python/efro/entity/__pycache__/_support.cpython-38.opt-1.pyc",
"ba_data/python/efro/entity/__pycache__/_value.cpython-38.opt-1.pyc",
"ba_data/python/efro/entity/__pycache__/util.cpython-38.opt-1.pyc",
"ba_data/python/efro/entity/_base.py",
"ba_data/python/efro/entity/_entity.py",
"ba_data/python/efro/entity/_field.py",
@ -479,6 +519,6 @@
"ba_data/python/efro/json.py",
"ba_data/python/efro/terminal.py",
"ba_data/python/efro/util.py",
"server/__pycache__/ballisticacore_server.cpython-37.opt-1.pyc",
"server/__pycache__/ballisticacore_server.cpython-38.opt-1.pyc",
"server/ballisticacore_server.py"
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,90 +1,83 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""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=unused-import
# pylint: disable=redefined-builtin
from _ba import (CollideModel, Context, ContextCall, Data, InputDevice,
Material, Model, Node, Player, Sound, Texture, Timer, Vec3,
Widget, buttonwidget, camerashake, checkboxwidget,
columnwidget, containerwidget, do_once, emitfx,
get_collision_info, getactivity, getcollidemodel, getmodel,
getnodes, getsession, getsound, gettexture, hscrollwidget,
imagewidget, log, new_activity, newnode, playsound,
printnodes, printobjects, pushcall, quit, rowwidget,
safecolor, screenmessage, scrollwidget, set_analytics_screen,
charstr, textwidget, time, timer, open_url, widget)
from _ba import (
CollideModel, Context, ContextCall, Data, InputDevice, Material, Model,
Node, SessionPlayer, Sound, Texture, Timer, Vec3, Widget, buttonwidget,
camerashake, checkboxwidget, columnwidget, containerwidget, do_once,
emitfx, getactivity, getcollidemodel, getmodel, getnodes, getsession,
getsound, gettexture, hscrollwidget, imagewidget, log, newactivity,
newnode, playsound, printnodes, printobjects, pushcall, quit, rowwidget,
safecolor, screenmessage, scrollwidget, set_analytics_screen, charstr,
textwidget, time, timer, open_url, widget, clipboard_is_supported,
clipboard_has_text, clipboard_get_text, clipboard_set_text)
from ba._activity import Activity
from ba._plugin import PotentialPlugin, Plugin, PluginSubsystem
from ba._actor import Actor
from ba._player import PlayerInfo, Player, EmptyPlayer, StandLocation
from ba._nodeactor import NodeActor
from ba._app import App
from ba._coopgame import CoopGameActivity
from ba._coopsession import CoopSession
from ba._dependency import (Dependency, DependencyComponent, DependencySet,
AssetPackage)
from ba._enums import TimeType, Permission, TimeFormat, SpecialChar
from ba._error import (UNHANDLED, print_exception, print_error, NotFoundError,
PlayerNotFoundError, NodeNotFoundError,
ActorNotFoundError, InputDeviceNotFoundError,
WidgetNotFoundError, ActivityNotFoundError,
TeamNotFoundError, SessionNotFoundError,
DependencyError)
from ba._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, SessionTeamNotFoundError,
SessionNotFoundError, DelegateNotFoundError, DependencyError)
from ba._freeforallsession import FreeForAllSession
from ba._gameactivity import GameActivity
from ba._gameresults import TeamGameResults
from ba._lang import Lstr, setlanguage, get_valid_languages
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 Team
from ba._team import SessionTeam, Team, EmptyTeam
from ba._teamgame import TeamGameActivity
from ba._dualteamsession import DualTeamSession
from ba._achievement import Achievement
from ba._achievement import Achievement, AchievementSubsystem
from ba._appconfig import AppConfig
from ba._appdelegate import AppDelegate
from ba._apputils import is_browser_likely_available
from ba._apputils import is_browser_likely_available, garbage_collect
from ba._campaign import Campaign
from ba._gameutils import (animate, animate_array, show_damage_count,
sharedobj, timestring, cameraflash)
from ba._general import WeakCall, Call
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._messages import (OutOfBoundsMessage, DeathType, DieMessage,
StandMessage, PickUpMessage, DropMessage,
PickedUpMessage, DroppedMessage,
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
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

View File

@ -1,218 +1,267 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Account related functionality."""
from __future__ import annotations
import copy
import time
from typing import TYPE_CHECKING
import _ba
if TYPE_CHECKING:
from typing import Any, Optional, Dict, List
from typing import Any, Optional, Dict, List, Tuple
import ba
def handle_account_gained_tickets(count: int) -> None:
"""Called when the current account has been awarded tickets.
class AccountSubsystem:
"""Subsystem for account handling in the app.
(internal)
Category: App Classes
Access the single shared instance of this class at 'ba.app.plugins'.
"""
from ba._lang import Lstr
_ba.screenmessage(Lstr(resource='getTicketsWindow.receivedTicketsText',
subs=[('${COUNT}', str(count))]),
color=(0, 1, 0))
_ba.playsound(_ba.getsound('cashRegister'))
def __init__(self) -> None:
self.account_tournament_list: Optional[Tuple[int, List[str]]] = None
def cache_league_rank_data(data: Any) -> None:
"""(internal)"""
_ba.app.league_rank_cache['info'] = copy.deepcopy(data)
# FIXME: should abstract/structure these.
self.tournament_info: Dict = {}
self.league_rank_cache: Dict = {}
self.last_post_purchase_message_time: Optional[float] = None
# If we try to run promo-codes due to launch-args/etc we might
# not be signed in yet; go ahead and queue them up in that case.
self.pending_promo_codes: List[str] = []
def get_cached_league_rank_data() -> Any:
"""(internal)"""
return _ba.app.league_rank_cache.get('info', None)
def on_app_launch(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 _ba.app.headless_mode or _ba.app.config.get(
'Auto Account State') == 'Local':
_ba.sign_in('Local')
def get_league_rank_points(data: Optional[Dict[str, Any]],
subset: str = None) -> int:
"""(internal)"""
if data is None:
return 0
_ba.pushcall(do_auto_sign_in)
# If the data contains an achievement total, use that. otherwise calc
# locally.
if data['at'] is not None:
total_ach_value = data['at']
else:
total_ach_value = 0
for ach in _ba.app.achievements:
if ach.complete:
total_ach_value += ach.power_ranking_value
def on_app_resume(self) -> None:
"""Should be called when the app is resumed."""
trophies_total: int = (data['t0a'] * data['t0am'] +
data['t0b'] * data['t0bm'] +
data['t1'] * data['t1m'] +
data['t2'] * data['t2m'] +
data['t3'] * data['t3m'] + data['t4'] * data['t4m'])
if subset == 'trophyCount':
val: int = (data['t0a'] + data['t0b'] + data['t1'] + data['t2'] +
data['t3'] + data['t4'])
assert isinstance(val, int)
return val
if subset == 'trophies':
assert isinstance(trophies_total, int)
return trophies_total
if subset is not None:
raise Exception('invalid subset value: ' + str(subset))
# Mark our cached tourneys as invalid so anyone using them knows
# they might be out of date.
for entry in list(self.tournament_info.values()):
entry['valid'] = False
if data['p']:
pro_mult = 1.0 + float(
_ba.get_account_misc_read_val('proPowerRankingBoost', 0.0)) * 0.01
else:
pro_mult = 1.0
def handle_account_gained_tickets(self, count: int) -> None:
"""Called when the current account has been awarded tickets.
# For final value, apply our pro mult and activeness-mult.
return int((total_ach_value + trophies_total) *
(data['act'] if data['act'] is not None else 1.0) * pro_mult)
(internal)
"""
from ba._language import Lstr
_ba.screenmessage(Lstr(resource='getTicketsWindow.receivedTicketsText',
subs=[('${COUNT}', str(count))]),
color=(0, 1, 0))
_ba.playsound(_ba.getsound('cashRegister'))
def cache_league_rank_data(self, data: Any) -> None:
"""(internal)"""
self.league_rank_cache['info'] = copy.deepcopy(data)
def cache_tournament_info(info: Any) -> None:
"""(internal)"""
from ba._enums import TimeType, TimeFormat
for entry in info:
cache_entry = _ba.app.tournament_info[entry['tournamentID']] = (
copy.deepcopy(entry))
def get_cached_league_rank_data(self) -> Any:
"""(internal)"""
return self.league_rank_cache.get('info', None)
# 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['valid'] = True
def get_league_rank_points(self,
data: Optional[Dict[str, Any]],
subset: str = None) -> int:
"""(internal)"""
if data is None:
return 0
# If the data contains an achievement total, use that. otherwise calc
# locally.
if data['at'] is not None:
total_ach_value = data['at']
else:
total_ach_value = 0
for ach in _ba.app.ach.achievements:
if ach.complete:
total_ach_value += ach.power_ranking_value
def get_purchased_icons() -> List[str]:
"""(internal)"""
# pylint: disable=cyclic-import
from ba import _store
if _ba.get_account_state() != 'signed_in':
return []
icons = []
store_items = _store.get_store_items()
for item_name, item in list(store_items.items()):
if item_name.startswith('icons.') and _ba.get_purchased(item_name):
icons.append(item['icon'])
return icons
trophies_total: int = (data['t0a'] * data['t0am'] +
data['t0b'] * data['t0bm'] +
data['t1'] * data['t1m'] +
data['t2'] * data['t2m'] +
data['t3'] * data['t3m'] +
data['t4'] * data['t4m'])
if subset == 'trophyCount':
val: int = (data['t0a'] + data['t0b'] + data['t1'] + data['t2'] +
data['t3'] + data['t4'])
assert isinstance(val, int)
return val
if subset == 'trophies':
assert isinstance(trophies_total, int)
return trophies_total
if subset is not None:
raise ValueError('invalid subset value: ' + str(subset))
if data['p']:
pro_mult = 1.0 + float(
_ba.get_account_misc_read_val('proPowerRankingBoost',
0.0)) * 0.01
else:
pro_mult = 1.0
def ensure_have_account_player_profile() -> None:
"""
Ensure the standard account-named player profile exists;
creating if needed.
"""
# This only applies when we're signed in.
if _ba.get_account_state() != 'signed_in':
return
# For final value, apply our pro mult and activeness-mult.
return int(
(total_ach_value + trophies_total) *
(data['act'] if data['act'] is not None else 1.0) * pro_mult)
# If the short version of our account name currently cant be
# displayed by the game, cancel.
if not _ba.have_chars(_ba.get_account_display_string(full=False)):
return
def cache_tournament_info(self, info: Any) -> None:
"""(internal)"""
from ba._enums import TimeType, TimeFormat
for entry in info:
cache_entry = self.tournament_info[entry['tournamentID']] = (
copy.deepcopy(entry))
config = _ba.app.config
if ('Player Profiles' not in config
or '__account__' not in config['Player Profiles']):
# 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['valid'] = True
# Create a spaz with a nice default purply color.
def get_purchased_icons(self) -> List[str]:
"""(internal)"""
# pylint: disable=cyclic-import
from ba import _store
if _ba.get_account_state() != 'signed_in':
return []
icons = []
store_items = _store.get_store_items()
for item_name, item in list(store_items.items()):
if item_name.startswith('icons.') and _ba.get_purchased(item_name):
icons.append(item['icon'])
return icons
def ensure_have_account_player_profile(self) -> None:
"""
Ensure the standard account-named player profile exists;
creating if needed.
(internal)
"""
# This only applies when we're signed in.
if _ba.get_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(_ba.get_account_display_string(full=False)):
return
config = _ba.app.config
if ('Player Profiles' not in config
or '__account__' not in config['Player Profiles']):
# Create a spaz with a nice default purply color.
_ba.add_transaction({
'type': 'ADD_PLAYER_PROFILE',
'name': '__account__',
'profile': {
'character': 'Spaz',
'color': [0.5, 0.25, 1.0],
'highlight': [0.5, 0.25, 1.0]
}
})
_ba.run_transactions()
def have_pro(self) -> bool:
"""Return whether pro is currently unlocked."""
# Check our tickets-based pro upgrade and our two real-IAP based
# upgrades. Also unlock this stuff in ballistica-core builds.
return bool(
_ba.get_purchased('upgrades.pro')
or _ba.get_purchased('static.pro')
or _ba.get_purchased('static.pro_sale')
or 'ballistica' + 'core' == _ba.appname())
def have_pro_options(self) -> bool:
"""Return whether pro-options are present.
This is True for owners of Pro or old installs
before Pro was a requirement for these.
"""
# 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(
_ba.get_account_misc_read_val_2('proOptionsUnlocked', False)
or _ba.app.config.get('lc14292', 0) > 1)
def show_post_purchase_message(self) -> None:
"""(internal)"""
from ba._language import Lstr
from ba._enums import TimeType
cur_time = _ba.time(TimeType.REAL)
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'))
def on_account_state_changed(self) -> None:
"""(internal)"""
from ba._language import Lstr
# Run any pending promo codes we had queued up while not signed in.
if _ba.get_account_state() == 'signed_in' and self.pending_promo_codes:
for code in self.pending_promo_codes:
_ba.screenmessage(Lstr(resource='submittingPromoCodeText'),
color=(0, 1, 0))
_ba.add_transaction({
'type': 'PROMO_CODE',
'expire_time': time.time() + 5,
'code': code
})
_ba.run_transactions()
self.pending_promo_codes = []
def add_pending_promo_code(self, code: str) -> None:
"""(internal)"""
from ba._language import Lstr
from ba._enums import TimeType
# 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 _ba.get_account_state() != 'signed_in':
def check_pending_codes() -> None:
"""(internal)"""
# 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(Lstr(resource='signInForPromoCodeText'),
color=(1, 0, 0))
_ba.playsound(_ba.getsound('error'))
self.pending_promo_codes.append(code)
_ba.timer(6.0, check_pending_codes, timetype=TimeType.REAL)
return
_ba.screenmessage(Lstr(resource='submittingPromoCodeText'),
color=(0, 1, 0))
_ba.add_transaction({
'type': 'ADD_PLAYER_PROFILE',
'name': '__account__',
'profile': {
'character': 'Spaz',
'color': [0.5, 0.25, 1.0],
'highlight': [0.5, 0.25, 1.0]
}
'type': 'PROMO_CODE',
'expire_time': time.time() + 5,
'code': code
})
_ba.run_transactions()
def have_pro() -> bool:
"""Return whether pro is currently unlocked."""
# Check our tickets-based pro upgrade and our two real-IAP based upgrades.
# Also unlock this stuff in ballistica-core builds.
return bool(
_ba.get_purchased('upgrades.pro') or _ba.get_purchased('static.pro')
or _ba.get_purchased('static.pro_sale')
or 'ballistica' + 'core' == 'ballisticacore')
def have_pro_options() -> bool:
"""Return whether pro-options are present.
This is True for owners of Pro or old installs
before Pro was a requirement for these.
"""
# 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 have_pro() or bool(
_ba.get_account_misc_read_val_2('proOptionsUnlocked', False)
or _ba.app.config.get('lc14292', 0) > 1)
def show_post_purchase_message() -> None:
"""(internal)"""
from ba._lang import Lstr
from ba._enums import TimeType
app = _ba.app
cur_time = _ba.time(TimeType.REAL)
if (app.last_post_purchase_message_time is None
or cur_time - app.last_post_purchase_message_time > 3.0):
app.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'))
def on_account_state_changed() -> None:
"""(internal)"""
import time
from ba import _lang
app = _ba.app
# Run any pending promo codes we had queued up while not signed in.
if _ba.get_account_state() == 'signed_in' and app.pending_promo_codes:
for code in app.pending_promo_codes:
_ba.screenmessage(_lang.Lstr(resource='submittingPromoCodeText'),
color=(0, 1, 0))
_ba.add_transaction({
'type': 'PROMO_CODE',
'expire_time': time.time() + 5,
'code': code
})
_ba.run_transactions()
app.pending_promo_codes = []

View File

@ -1,32 +1,15 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Various functionality related to achievements."""
from __future__ import annotations
from typing import TYPE_CHECKING
import _ba
from ba._error import print_exception
if TYPE_CHECKING:
from typing import Any, Sequence, List, Dict, Union, Optional
from typing import Any, Sequence, List, Dict, Union, Optional, Tuple, Set
import ba
# This could use some cleanup.
@ -79,69 +62,346 @@ ACH_LEVEL_NAMES = {
}
def award_local_achievement(achname: str) -> None:
"""For non-game-based achievements such as controller-connection ones."""
try:
ach = get_achievement(achname)
if not ach.complete:
class AchievementSubsystem:
"""Subsystem for achievement handling.
# Report new achievements to the game-service.
_ba.report_achievement(achname)
Category: App Classes
# And to our account.
_ba.add_transaction({'type': 'ACHIEVEMENT', 'name': achname})
# Now attempt to show a banner.
display_achievement_banner(achname)
except Exception:
from ba import _error
_error.print_exception()
def display_achievement_banner(achname: str) -> None:
"""Display a completion banner for an achievement.
Used for server-driven achievements.
Access the single shared instance of this class at 'ba.app.ach'.
"""
try:
# FIXME: Need to get these using the UI context or some other
# 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()
if activity is not None:
with _ba.Context(activity):
get_achievement(achname).announce_completion()
except Exception:
from ba import _error
_error.print_exception('error showing server ach')
def __init__(self) -> None:
self.achievements: List[Achievement] = []
self.achievements_to_display: (List[Tuple[ba.Achievement, bool]]) = []
self.achievement_display_timer: Optional[_ba.Timer] = None
self.last_achievement_display_time: float = 0.0
self.achievement_completion_banner_slots: Set[int] = set()
self._init_achievements()
# This gets called whenever game-center/game-circle/etc tells 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
def _init_achievements(self) -> None:
"""Fill in available achievements."""
achs = self.achievements
def set_completed_achievements(achs: Sequence[str]) -> None:
"""Set the current state of completed achievements.
# 5
achs.append(
Achievement('In Control', 'achievementInControl', (1, 1, 1), '',
5))
# 15
achs.append(
Achievement('Sharing is Caring', 'achievementSharingIsCaring',
(1, 1, 1), '', 15))
# 10
achs.append(
Achievement('Dual Wielding', 'achievementDualWielding', (1, 1, 1),
'', 10))
All achievements not included here will be set incomplete.
"""
cfg = _ba.app.config
cfg['Achievements'] = {}
for a_name in achs:
get_achievement(a_name).set_complete(True)
cfg.commit()
# 10
achs.append(
Achievement('Free Loader', 'achievementFreeLoader', (1, 1, 1), '',
10))
# 20
achs.append(
Achievement('Team Player', 'achievementTeamPlayer', (1, 1, 1), '',
20))
# 5
achs.append(
Achievement('Onslaught Training Victory', 'achievementOnslaught',
(1, 1, 1), 'Default:Onslaught Training', 5))
# 5
achs.append(
Achievement('Off You Go Then', 'achievementOffYouGo',
(1, 1.1, 1.3), 'Default:Onslaught Training', 5))
# 10
achs.append(
Achievement('Boxer',
'achievementBoxer', (1, 0.6, 0.6),
'Default:Onslaught Training',
10,
hard_mode_only=True))
def get_achievement(name: str) -> Achievement:
"""Return an Achievement by name."""
achs = [a for a in _ba.app.achievements if a.name == name]
assert len(achs) < 2
if not achs:
raise Exception("Invalid achievement name: '" + name + "'")
return achs[0]
# 10
achs.append(
Achievement('Rookie Onslaught Victory', 'achievementOnslaught',
(0.5, 1.4, 0.6), 'Default:Rookie Onslaught', 10))
# 10
achs.append(
Achievement('Mine Games', 'achievementMine', (1, 1, 1.4),
'Default:Rookie Onslaught', 10))
# 15
achs.append(
Achievement('Flawless Victory',
'achievementFlawlessVictory', (1, 1, 1),
'Default:Rookie Onslaught',
15,
hard_mode_only=True))
# 10
achs.append(
Achievement('Rookie Football Victory',
'achievementFootballVictory', (1.0, 1, 0.6),
'Default:Rookie Football', 10))
# 10
achs.append(
Achievement('Super Punch', 'achievementSuperPunch', (1, 1, 1.8),
'Default:Rookie Football', 10))
# 15
achs.append(
Achievement('Rookie Football Shutout',
'achievementFootballShutout', (1, 1, 1),
'Default:Rookie Football',
15,
hard_mode_only=True))
# 15
achs.append(
Achievement('Pro Onslaught Victory', 'achievementOnslaught',
(0.3, 1, 2.0), 'Default:Pro Onslaught', 15))
# 15
achs.append(
Achievement('Boom Goes the Dynamite', 'achievementTNT',
(1.4, 1.2, 0.8), 'Default:Pro Onslaught', 15))
# 20
achs.append(
Achievement('Pro Boxer',
'achievementBoxer', (2, 2, 0),
'Default:Pro Onslaught',
20,
hard_mode_only=True))
# 15
achs.append(
Achievement('Pro Football Victory', 'achievementFootballVictory',
(1.3, 1.3, 2.0), 'Default:Pro Football', 15))
# 15
achs.append(
Achievement('Super Mega Punch', 'achievementSuperPunch',
(2, 1, 0.6), 'Default:Pro Football', 15))
# 20
achs.append(
Achievement('Pro Football Shutout',
'achievementFootballShutout', (0.7, 0.7, 2.0),
'Default:Pro Football',
20,
hard_mode_only=True))
# 15
achs.append(
Achievement('Pro Runaround Victory', 'achievementRunaround',
(1, 1, 1), 'Default:Pro Runaround', 15))
# 20
achs.append(
Achievement('Precision Bombing',
'achievementCrossHair', (1, 1, 1.3),
'Default:Pro Runaround',
20,
hard_mode_only=True))
# 25
achs.append(
Achievement('The Wall',
'achievementWall', (1, 0.7, 0.7),
'Default:Pro Runaround',
25,
hard_mode_only=True))
# 30
achs.append(
Achievement('Uber Onslaught Victory', 'achievementOnslaught',
(2, 2, 1), 'Default:Uber Onslaught', 30))
# 30
achs.append(
Achievement('Gold Miner',
'achievementMine', (2, 1.6, 0.2),
'Default:Uber Onslaught',
30,
hard_mode_only=True))
# 30
achs.append(
Achievement('TNT Terror',
'achievementTNT', (2, 1.8, 0.3),
'Default:Uber Onslaught',
30,
hard_mode_only=True))
# 30
achs.append(
Achievement('Uber Football Victory', 'achievementFootballVictory',
(1.8, 1.4, 0.3), 'Default:Uber Football', 30))
# 30
achs.append(
Achievement('Got the Moves',
'achievementGotTheMoves', (2, 1, 0),
'Default:Uber Football',
30,
hard_mode_only=True))
# 40
achs.append(
Achievement('Uber Football Shutout',
'achievementFootballShutout', (2, 2, 0),
'Default:Uber Football',
40,
hard_mode_only=True))
# 30
achs.append(
Achievement('Uber Runaround Victory', 'achievementRunaround',
(1.5, 1.2, 0.2), 'Default:Uber Runaround', 30))
# 40
achs.append(
Achievement('The Great Wall',
'achievementWall', (2, 1.7, 0.4),
'Default:Uber Runaround',
40,
hard_mode_only=True))
# 40
achs.append(
Achievement('Stayin\' Alive',
'achievementStayinAlive', (2, 2, 1),
'Default:Uber Runaround',
40,
hard_mode_only=True))
# 20
achs.append(
Achievement('Last Stand Master',
'achievementMedalSmall', (2, 1.5, 0.3),
'Default:The Last Stand',
20,
hard_mode_only=True))
# 40
achs.append(
Achievement('Last Stand Wizard',
'achievementMedalMedium', (2, 1.5, 0.3),
'Default:The Last Stand',
40,
hard_mode_only=True))
# 60
achs.append(
Achievement('Last Stand God',
'achievementMedalLarge', (2, 1.5, 0.3),
'Default:The Last Stand',
60,
hard_mode_only=True))
# 5
achs.append(
Achievement('Onslaught Master', 'achievementMedalSmall',
(0.7, 1, 0.7), 'Challenges:Infinite Onslaught', 5))
# 15
achs.append(
Achievement('Onslaught Wizard', 'achievementMedalMedium',
(0.7, 1.0, 0.7), 'Challenges:Infinite Onslaught', 15))
# 30
achs.append(
Achievement('Onslaught God', 'achievementMedalLarge',
(0.7, 1.0, 0.7), 'Challenges:Infinite Onslaught', 30))
# 5
achs.append(
Achievement('Runaround Master', 'achievementMedalSmall',
(1.0, 1.0, 1.2), 'Challenges:Infinite Runaround', 5))
# 15
achs.append(
Achievement('Runaround Wizard', 'achievementMedalMedium',
(1.0, 1.0, 1.2), 'Challenges:Infinite Runaround', 15))
# 30
achs.append(
Achievement('Runaround God', 'achievementMedalLarge',
(1.0, 1.0, 1.2), 'Challenges:Infinite Runaround', 30))
def award_local_achievement(self, achname: str) -> None:
"""For non-game-based achievements such as controller-connection."""
try:
ach = self.get_achievement(achname)
if not ach.complete:
# Report new achievements to the game-service.
_ba.report_achievement(achname)
# And to our account.
_ba.add_transaction({'type': 'ACHIEVEMENT', 'name': achname})
# Now attempt to show a banner.
self.display_achievement_banner(achname)
except Exception:
print_exception()
def display_achievement_banner(self, achname: str) -> None:
"""Display a completion banner for an achievement.
(internal)
Used for server-driven achievements.
"""
try:
# FIXME: Need to get these using the UI context or some other
# 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()
if activity is not None:
with _ba.Context(activity):
self.get_achievement(achname).announce_completion()
except Exception:
print_exception('error showing server ach')
def set_completed_achievements(self, achs: Sequence[str]) -> None:
"""Set the current state of completed achievements.
(internal)
All achievements not included here will be set incomplete.
"""
# Note: This gets called whenever game-center/game-circle/etc tells
# 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['Achievements'] = {}
for a_name in achs:
self.get_achievement(a_name).set_complete(True)
cfg.commit()
def get_achievement(self, name: str) -> Achievement:
"""Return an Achievement by name."""
achs = [a for a in self.achievements if a.name == name]
assert len(achs) < 2
if not achs:
raise ValueError("Invalid achievement name: '" + name + "'")
return achs[0]
def achievements_for_coop_level(self,
level_name: str) -> List[Achievement]:
"""Given a level name, return achievements available for it."""
# For the Easy campaign we return achievements for the Default
# campaign too. (want the user to see what achievements are part of the
# level even if they can't unlock them all on easy mode).
return [
a for a in self.achievements
if a.level_name in (level_name,
level_name.replace('Easy', 'Default'))
]
def _test(self) -> None:
"""For testing achievement animations."""
from ba._enums import TimeType
def testcall1() -> None:
self.achievements[0].announce_completion()
self.achievements[1].announce_completion()
self.achievements[2].announce_completion()
def testcall2() -> None:
self.achievements[3].announce_completion()
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)
def _get_ach_mult(include_pro_bonus: bool = False) -> int:
@ -149,42 +409,28 @@ def _get_ach_mult(include_pro_bonus: bool = False) -> int:
(just for display; changing this here won't affect actual rewards)
"""
from ba import _account
val: int = _ba.get_account_misc_read_val('achAwardMult', 5)
assert isinstance(val, int)
if include_pro_bonus and _account.have_pro():
if include_pro_bonus and _ba.app.accounts.have_pro():
val *= 2
return val
def get_achievements_for_coop_level(level_name: str) -> List[Achievement]:
"""Given a level name, return achievements available for it."""
# For the Easy campaign we return achievements for the Default
# campaign too. (want the user to see what achievements are part of the
# level even if they can't unlock them all on easy mode).
return [
a for a in _ba.app.achievements
if a.level_name in (level_name, level_name.replace('Easy', 'Default'))
]
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.achievements_to_display:
if app.ach.achievements_to_display:
try:
ach, sound = app.achievements_to_display.pop(0)
ach, sound = app.ach.achievements_to_display.pop(0)
ach.show_completion_banner(sound)
except Exception:
from ba import _error
_error.print_exception('error showing next achievement')
app.achievements_to_display = []
app.achievement_display_timer = None
print_exception('error showing next achievement')
app.ach.achievements_to_display = []
app.ach.achievement_display_timer = None
else:
app.achievement_display_timer = None
app.ach.achievement_display_timer = None
class Achievement:
@ -254,18 +500,18 @@ class Achievement:
return
# If we're being freshly complete, display/report it and whatnot.
if (self, sound) not in app.achievements_to_display:
app.achievements_to_display.append((self, sound))
if (self, sound) not in app.ach.achievements_to_display:
app.ach.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).
# 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.achievement_display_timer is None or
_ba.time(TimeType.REAL) - app.last_achievement_display_time > 2.0)
and _ba.getactivity(doraise=False) is not None):
app.achievement_display_timer = _ba.Timer(
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,
@ -287,36 +533,37 @@ class Achievement:
@property
def display_name(self) -> ba.Lstr:
"""Return a ba.Lstr for this Achievement's name."""
from ba._lang import Lstr
from ba._language import Lstr
name: Union[ba.Lstr, str]
try:
if self._level_name != '':
from ba._campaign import get_campaign
from ba._campaign import getcampaign
campaignname, campaign_level = self._level_name.split(':')
name = get_campaign(campaignname).get_level(
name = getcampaign(campaignname).getlevel(
campaign_level).displayname
else:
name = ''
except Exception:
from ba import _error
name = ''
_error.print_exception()
print_exception()
return Lstr(resource='achievements.' + self._name + '.name',
subs=[('${LEVEL}', name)])
@property
def description(self) -> ba.Lstr:
"""Get a ba.Lstr for the Achievement's brief description."""
from ba._lang import Lstr, get_resource
if 'description' in get_resource('achievements')[self._name]:
from ba._language import Lstr
if 'description' in _ba.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._lang import Lstr, get_resource
if 'descriptionComplete' in get_resource('achievements')[self._name]:
from ba._language import Lstr
if 'descriptionComplete' in _ba.app.lang.get_resource('achievements')[
self._name]:
return Lstr(resource='achievements.' + self._name +
'.descriptionComplete')
return Lstr(resource='achievements.' + self._name +
@ -325,7 +572,7 @@ class Achievement:
@property
def description_full(self) -> ba.Lstr:
"""Get a ba.Lstr for the Achievement's full description."""
from ba._lang import Lstr
from ba._language import Lstr
return Lstr(
resource='achievements.' + self._name + '.descriptionFull',
@ -336,7 +583,7 @@ class Achievement:
@property
def description_full_complete(self) -> ba.Lstr:
"""Get a ba.Lstr for the Achievement's full desc. when completed."""
from ba._lang import Lstr
from ba._language import Lstr
return Lstr(
resource='achievements.' + self._name + '.descriptionFullComplete',
subs=[('${LEVEL}',
@ -371,8 +618,9 @@ class Achievement:
Shows the Achievement icon, name, and description.
"""
# pylint: disable=cyclic-import
from ba._lang import Lstr
from ba._language import Lstr
from ba._enums import SpecialChar
from ba._coopsession import CoopSession
from bastd.actor.image import Image
from bastd.actor.text import Text
@ -396,7 +644,7 @@ class Achievement:
v_attach = Text.VAttach.TOP
attach = Image.Attach.TOP_CENTER
else:
raise Exception('invalid style "' + style + '"')
raise ValueError('invalid style "' + style + '"')
# Attempt to determine what campaign we're in
# (so we know whether to show "hard mode only").
@ -404,12 +652,15 @@ class Achievement:
hmo = False
else:
try:
campaign = _ba.getsession().campaign
assert campaign is not None
hmo = (self._hard_mode_only and campaign.name == 'Easy')
session = _ba.getsession()
if isinstance(session, CoopSession):
campaign = session.campaign
assert campaign is not None
hmo = (self._hard_mode_only and campaign.name == 'Easy')
else:
hmo = False
except Exception:
from ba import _error
_error.print_exception('unable to determine campaign')
print_exception('Error determining campaign.')
hmo = False
objs: List[ba.Actor]
@ -659,7 +910,7 @@ class Achievement:
def _remove_banner_slot(self) -> None:
assert self._completion_banner_slot is not None
_ba.app.achievement_completion_banner_slots.remove(
_ba.app.ach.achievement_completion_banner_slots.remove(
self._completion_banner_slot)
self._completion_banner_slot = None
@ -670,15 +921,15 @@ class Achievement:
from bastd.actor.text import Text
from bastd.actor.image import Image
from ba._general import WeakCall
from ba._lang import Lstr
from ba._language import Lstr
from ba._messages import DieMessage
from ba._enums import TimeType, SpecialChar
app = _ba.app
app.last_achievement_display_time = _ba.time(TimeType.REAL)
app.ach.last_achievement_display_time = _ba.time(TimeType.REAL)
# Just piggy-back onto any current activity
# (should we use the session instead?..)
activity: Optional[ba.Activity] = _ba.getactivity(doraise=False)
activity = _ba.getactivity(doraise=False)
# If this gets called while this achievement is occupying a slot
# already, ignore it. (probably should never happen in real
@ -705,8 +956,8 @@ class Achievement:
# Find the first free slot.
i = 0
while True:
if i not in app.achievement_completion_banner_slots:
app.achievement_completion_banner_slots.add(i)
if i not in app.ach.achievement_completion_banner_slots:
app.ach.achievement_completion_banner_slots.add(i)
self._completion_banner_slot = i
# Remove us from that slot when we close.
@ -926,7 +1177,7 @@ class Achievement:
objt.node.host_only = True
# Add the 'x 2' if we've got pro.
if _account.have_pro():
if app.accounts.have_pro():
objt = Text('x 2',
position=(-120 - 180 + 45, 80 + y_offs - 50),
v_attach=Text.VAttach.BOTTOM,
@ -964,252 +1215,3 @@ class Achievement:
for actor in objs:
_ba.timer(out_time + 1.000,
WeakCall(actor.handlemessage, DieMessage()))
def init_achievements() -> None:
"""Fill in available achievements."""
achs = _ba.app.achievements
# 5
achs.append(
Achievement('In Control', 'achievementInControl', (1, 1, 1), '', 5))
# 15
achs.append(
Achievement('Sharing is Caring', 'achievementSharingIsCaring',
(1, 1, 1), '', 15))
# 10
achs.append(
Achievement('Dual Wielding', 'achievementDualWielding', (1, 1, 1), '',
10))
# 10
achs.append(
Achievement('Free Loader', 'achievementFreeLoader', (1, 1, 1), '', 10))
# 20
achs.append(
Achievement('Team Player', 'achievementTeamPlayer', (1, 1, 1), '', 20))
# 5
achs.append(
Achievement('Onslaught Training Victory', 'achievementOnslaught',
(1, 1, 1), 'Default:Onslaught Training', 5))
# 5
achs.append(
Achievement('Off You Go Then', 'achievementOffYouGo', (1, 1.1, 1.3),
'Default:Onslaught Training', 5))
# 10
achs.append(
Achievement('Boxer',
'achievementBoxer', (1, 0.6, 0.6),
'Default:Onslaught Training',
10,
hard_mode_only=True))
# 10
achs.append(
Achievement('Rookie Onslaught Victory', 'achievementOnslaught',
(0.5, 1.4, 0.6), 'Default:Rookie Onslaught', 10))
# 10
achs.append(
Achievement('Mine Games', 'achievementMine', (1, 1, 1.4),
'Default:Rookie Onslaught', 10))
# 15
achs.append(
Achievement('Flawless Victory',
'achievementFlawlessVictory', (1, 1, 1),
'Default:Rookie Onslaught',
15,
hard_mode_only=True))
# 10
achs.append(
Achievement('Rookie Football Victory', 'achievementFootballVictory',
(1.0, 1, 0.6), 'Default:Rookie Football', 10))
# 10
achs.append(
Achievement('Super Punch', 'achievementSuperPunch', (1, 1, 1.8),
'Default:Rookie Football', 10))
# 15
achs.append(
Achievement('Rookie Football Shutout',
'achievementFootballShutout', (1, 1, 1),
'Default:Rookie Football',
15,
hard_mode_only=True))
# 15
achs.append(
Achievement('Pro Onslaught Victory', 'achievementOnslaught',
(0.3, 1, 2.0), 'Default:Pro Onslaught', 15))
# 15
achs.append(
Achievement('Boom Goes the Dynamite', 'achievementTNT',
(1.4, 1.2, 0.8), 'Default:Pro Onslaught', 15))
# 20
achs.append(
Achievement('Pro Boxer',
'achievementBoxer', (2, 2, 0),
'Default:Pro Onslaught',
20,
hard_mode_only=True))
# 15
achs.append(
Achievement('Pro Football Victory', 'achievementFootballVictory',
(1.3, 1.3, 2.0), 'Default:Pro Football', 15))
# 15
achs.append(
Achievement('Super Mega Punch', 'achievementSuperPunch', (2, 1, 0.6),
'Default:Pro Football', 15))
# 20
achs.append(
Achievement('Pro Football Shutout',
'achievementFootballShutout', (0.7, 0.7, 2.0),
'Default:Pro Football',
20,
hard_mode_only=True))
# 15
achs.append(
Achievement('Pro Runaround Victory', 'achievementRunaround', (1, 1, 1),
'Default:Pro Runaround', 15))
# 20
achs.append(
Achievement('Precision Bombing',
'achievementCrossHair', (1, 1, 1.3),
'Default:Pro Runaround',
20,
hard_mode_only=True))
# 25
achs.append(
Achievement('The Wall',
'achievementWall', (1, 0.7, 0.7),
'Default:Pro Runaround',
25,
hard_mode_only=True))
# 30
achs.append(
Achievement('Uber Onslaught Victory', 'achievementOnslaught',
(2, 2, 1), 'Default:Uber Onslaught', 30))
# 30
achs.append(
Achievement('Gold Miner',
'achievementMine', (2, 1.6, 0.2),
'Default:Uber Onslaught',
30,
hard_mode_only=True))
# 30
achs.append(
Achievement('TNT Terror',
'achievementTNT', (2, 1.8, 0.3),
'Default:Uber Onslaught',
30,
hard_mode_only=True))
# 30
achs.append(
Achievement('Uber Football Victory', 'achievementFootballVictory',
(1.8, 1.4, 0.3), 'Default:Uber Football', 30))
# 30
achs.append(
Achievement('Got the Moves',
'achievementGotTheMoves', (2, 1, 0),
'Default:Uber Football',
30,
hard_mode_only=True))
# 40
achs.append(
Achievement('Uber Football Shutout',
'achievementFootballShutout', (2, 2, 0),
'Default:Uber Football',
40,
hard_mode_only=True))
# 30
achs.append(
Achievement('Uber Runaround Victory', 'achievementRunaround',
(1.5, 1.2, 0.2), 'Default:Uber Runaround', 30))
# 40
achs.append(
Achievement('The Great Wall',
'achievementWall', (2, 1.7, 0.4),
'Default:Uber Runaround',
40,
hard_mode_only=True))
# 40
achs.append(
Achievement('Stayin\' Alive',
'achievementStayinAlive', (2, 2, 1),
'Default:Uber Runaround',
40,
hard_mode_only=True))
# 20
achs.append(
Achievement('Last Stand Master',
'achievementMedalSmall', (2, 1.5, 0.3),
'Default:The Last Stand',
20,
hard_mode_only=True))
# 40
achs.append(
Achievement('Last Stand Wizard',
'achievementMedalMedium', (2, 1.5, 0.3),
'Default:The Last Stand',
40,
hard_mode_only=True))
# 60
achs.append(
Achievement('Last Stand God',
'achievementMedalLarge', (2, 1.5, 0.3),
'Default:The Last Stand',
60,
hard_mode_only=True))
# 5
achs.append(
Achievement('Onslaught Master', 'achievementMedalSmall', (0.7, 1, 0.7),
'Challenges:Infinite Onslaught', 5))
# 15
achs.append(
Achievement('Onslaught Wizard', 'achievementMedalMedium',
(0.7, 1.0, 0.7), 'Challenges:Infinite Onslaught', 15))
# 30
achs.append(
Achievement('Onslaught God', 'achievementMedalLarge', (0.7, 1.0, 0.7),
'Challenges:Infinite Onslaught', 30))
# 5
achs.append(
Achievement('Runaround Master', 'achievementMedalSmall',
(1.0, 1.0, 1.2), 'Challenges:Infinite Runaround', 5))
# 15
achs.append(
Achievement('Runaround Wizard', 'achievementMedalMedium',
(1.0, 1.0, 1.2), 'Challenges:Infinite Runaround', 15))
# 30
achs.append(
Achievement('Runaround God', 'achievementMedalLarge', (1.0, 1.0, 1.2),
'Challenges:Infinite Runaround', 30))
def _test() -> None:
"""For testing achievement animations."""
from ba._enums import TimeType
def testcall1() -> None:
app = _ba.app
app.achievements[0].announce_completion()
app.achievements[1].announce_completion()
app.achievements[2].announce_completion()
def testcall2() -> None:
app = _ba.app
app.achievements[3].announce_completion()
app.achievements[4].announce_completion()
app.achievements[5].announce_completion()
_ba.timer(3.0, testcall1, timetype=TimeType.BASE)
_ba.timer(7.0, testcall2, timetype=TimeType.BASE)

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Some handy base class and special purpose Activity types."""
from __future__ import annotations
@ -26,6 +8,10 @@ from typing import TYPE_CHECKING
import _ba
from ba._activity import Activity
from ba._music import setmusic, MusicType
from ba._enums import InputType, UIScale
# 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
if TYPE_CHECKING:
from typing import Any, Dict, Optional
@ -33,17 +19,17 @@ if TYPE_CHECKING:
from ba._lobby import JoinInfo
class EndSessionActivity(Activity):
class EndSessionActivity(Activity[EmptyPlayer, EmptyTeam]):
"""Special ba.Activity to fade out and end the current ba.Session."""
def __init__(self, settings: Dict[str, Any]):
def __init__(self, settings: dict):
super().__init__(settings)
# Keeps prev activity alive while we fade out.
self.transition_time = 0.25
self.inherits_tint = True
self.inherits_slow_motion = True
self.inherits_camera_vr_offset = True
self.inherits_vr_camera_offset = True
self.inherits_vr_overlay_center = True
def on_transition_in(self) -> None:
@ -54,20 +40,19 @@ class EndSessionActivity(Activity):
def on_begin(self) -> None:
# pylint: disable=cyclic-import
from bastd.mainmenu import MainMenuSession
from ba._apputils import call_after_ad
from ba._general import Call
super().on_begin()
_ba.unlock_all_input()
call_after_ad(Call(_ba.new_host_session, MainMenuSession))
_ba.app.ads.call_after_ad(Call(_ba.new_host_session, MainMenuSession))
class JoinActivity(Activity):
class JoinActivity(Activity[EmptyPlayer, EmptyTeam]):
"""Standard activity for waiting for players to join.
It shows tips and other info and waits for all players to check ready.
"""
def __init__(self, settings: Dict[str, Any]):
def __init__(self, settings: dict):
super().__init__(settings)
# This activity is a special 'joiner' activity.
@ -98,22 +83,22 @@ class JoinActivity(Activity):
_ba.set_analytics_screen('Joining Screen')
class TransitionActivity(Activity):
"""A simple overlay fade out/in.
class TransitionActivity(Activity[EmptyPlayer, EmptyTeam]):
"""A simple overlay to fade out/in.
Useful as a bare minimum transition between two level based activities.
"""
def __init__(self, settings: Dict[str, Any]):
super().__init__(settings)
# Keep prev activity alive while we fade in.
transition_time = 0.5
inherits_slow_motion = True # Don't change.
inherits_tint = True # Don't change.
inherits_vr_camera_offset = True # Don't change.
inherits_vr_overlay_center = True
use_fixed_vr_overlay = True
# Keep prev activity alive while we fade in.
self.transition_time = 0.5
self.inherits_slow_motion = True # Don't change.
self.inherits_tint = True # Don't change.
self.inherits_camera_vr_offset = True # Don't change.
self.inherits_vr_overlay_center = True
self.use_fixed_vr_overlay = True
def __init__(self, settings: dict):
super().__init__(settings)
self._background: Optional[ba.Actor] = None
def on_transition_in(self) -> None:
@ -131,19 +116,21 @@ class TransitionActivity(Activity):
_ba.timer(0.1, self.end)
class ScoreScreenActivity(Activity):
class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]):
"""A standard score screen that fades in and shows stuff for a while.
After a specified delay, player input is assigned to end the activity.
"""
def __init__(self, settings: Dict[str, Any]):
transition_time = 0.5
inherits_tint = True
inherits_vr_camera_offset = True
use_fixed_vr_overlay = True
default_music: Optional[MusicType] = MusicType.SCORES
def __init__(self, settings: dict):
super().__init__(settings)
self.transition_time = 0.5
self.inherits_tint = True
self.inherits_camera_vr_offset = True
self.use_fixed_vr_overlay = True
self.default_music: Optional[MusicType] = MusicType.SCORES
self._birth_time = _ba.time()
self._min_view_time = 5.0
self._allow_server_transition = False
@ -155,16 +142,15 @@ class ScoreScreenActivity(Activity):
self._custom_continue_message: Optional[ba.Lstr] = None
self._server_transitioning: Optional[bool] = None
def on_player_join(self, player: ba.Player) -> None:
from ba import _general
def on_player_join(self, player: EmptyPlayer) -> None:
from ba._general import WeakCall
super().on_player_join(player)
time_till_assign = max(
0, self._birth_time + self._min_view_time - _ba.time())
# 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, _general.WeakCall(self._safe_assign,
player))
_ba.timer(time_till_assign, WeakCall(self._safe_assign, player))
def on_transition_in(self) -> None:
from bastd.actor.tipstext import TipsText
@ -180,18 +166,18 @@ class ScoreScreenActivity(Activity):
def on_begin(self) -> None:
# pylint: disable=cyclic-import
from bastd.actor.text import Text
from ba import _lang
from ba 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.interface_type == 'large':
if _ba.app.ui.uiscale is UIScale.LARGE:
# FIXME: Need a better way to determine whether we've probably
# got a keyboard.
sval = _lang.Lstr(resource='pressAnyKeyButtonText')
sval = _language.Lstr(resource='pressAnyKeyButtonText')
else:
sval = _lang.Lstr(resource='pressAnyButtonText')
sval = _language.Lstr(resource='pressAnyButtonText')
Text(self._custom_continue_message
if self._custom_continue_message is not None else sval,
@ -221,11 +207,11 @@ class ScoreScreenActivity(Activity):
# Otherwise end the activity normally.
self.end()
def _safe_assign(self, player: ba.Player) -> None:
def _safe_assign(self, player: EmptyPlayer) -> None:
# Just to be extra careful, don't assign if we're transitioning out.
# (though theoretically that would be ok).
# (though theoretically that should be ok).
if not self.is_transitioning_out() and player:
player.assign_input_call(
('jumpPress', 'punchPress', 'bombPress', 'pickUpPress'),
self._player_press)
player.assigninput((InputType.JUMP_PRESS, InputType.PUNCH_PRESS,
InputType.BOMB_PRESS, InputType.PICK_UP_PRESS),
self._player_press)

View File

@ -1,37 +1,18 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Defines base Actor class."""
from __future__ import annotations
import weakref
from typing import TYPE_CHECKING, TypeVar
from typing import TYPE_CHECKING, TypeVar, overload
from ba._messages import DieMessage, DeathType, OutOfBoundsMessage
from ba import _error
from ba._messages import DieMessage, DeathType, OutOfBoundsMessage, UNHANDLED
from ba._error import print_exception, ActivityNotFoundError
import _ba
if TYPE_CHECKING:
from typing import Any, Optional
from typing import Any, Optional, Literal
import ba
T = TypeVar('T', bound='Actor')
@ -45,7 +26,8 @@ class Actor:
Actors act as controllers, combining some number of ba.Nodes,
ba.Textures, ba.Sounds, etc. into a high-level cohesive unit.
Some example actors include Bomb, Flag, and Spaz classes in bastd.
Some example actors include the Bomb, Flag, and Spaz classes that
live in the bastd.actor.* modules.
One key feature of Actors is that they generally 'die'
(killing off or transitioning out their nodes) when the last Python
@ -94,24 +76,23 @@ class Actor:
def __del__(self) -> None:
try:
# Non-expired Actors send themselves a DieMessage when going down.
# Unexpired Actors send themselves a DieMessage when going down.
# That way we can treat DieMessage handling as the single
# point-of-action for death.
if not self.is_expired():
if not self.expired:
self.handlemessage(DieMessage())
except Exception:
_error.print_exception('exception in ba.Actor.__del__() for', self)
print_exception('exception in ba.Actor.__del__() for', self)
def handlemessage(self, msg: Any) -> Any:
"""General message handling; can be passed any message object."""
if __debug__:
self._handlemessage_sanity_check()
assert not self.expired
# By default, actors going out-of-bounds simply kill themselves.
if isinstance(msg, OutOfBoundsMessage):
return self.handlemessage(DieMessage(how=DeathType.OUT_OF_BOUNDS))
return _error.UNHANDLED
return UNHANDLED
def autoretain(self: T) -> T:
"""Keep this Actor alive without needing to hold a reference to it.
@ -126,16 +107,16 @@ class Actor:
"""
activity = self._activity()
if activity is None:
raise _error.ActivityNotFoundError()
raise ActivityNotFoundError()
activity.retain_actor(self)
return self
def on_expire(self) -> None:
"""Called for remaining ba.Actors when their ba.Activity shuts down.
Actors can use this opportunity to clear callbacks
or other references which have the potential of keeping the
ba.Activity alive inadvertently (Activities can not exit cleanly while
Actors can use this opportunity to clear callbacks or other
references which have the potential of keeping the ba.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
@ -144,13 +125,14 @@ class Actor:
likely result in errors.
"""
def is_expired(self) -> bool:
"""Returns whether the Actor is expired.
@property
def expired(self) -> bool:
"""Whether the Actor is expired.
(see ba.Actor.on_expire())
"""
activity = self.getactivity(doraise=False)
return True if activity is None else activity.is_expired()
return True if activity is None else activity.expired
def exists(self) -> bool:
"""Returns whether the Actor is still present in a meaningful way.
@ -169,7 +151,6 @@ class Actor:
so a simple "if myactor" test will conveniently do the right thing
even if myactor is set to None.
"""
return True
def __bool__(self) -> bool:
@ -186,23 +167,6 @@ class Actor:
"""
return True
def _handlemessage_sanity_check(self) -> None:
"""Make sure things are kosher in handlemessage().
Place this in an 'if __debug__:' clause at the top of handlemessage()
overrides. This will will complain if anything is sending the Actor
messages after the activity has ended, which should be explicitly
avoided.
"""
if not __debug__:
_error.print_error('This should only be called in __debug__ mode.',
once=True)
if not getattr(self, '_root_actor_init_called', False):
_error.print_error('Root Actor __init__() not called.')
if self.is_expired():
_error.print_error(
f'handlemessage() called on expired actor: {self}')
@property
def activity(self) -> ba.Activity:
"""The Activity this Actor was created in.
@ -211,16 +175,26 @@ class Actor:
"""
activity = self._activity()
if activity is None:
raise _error.ActivityNotFoundError()
raise ActivityNotFoundError()
return activity
# Overloads to convey our exact return type depending on 'doraise' value.
@overload
def getactivity(self, doraise: Literal[True] = True) -> ba.Activity:
...
@overload
def getactivity(self, doraise: Literal[False]) -> Optional[ba.Activity]:
...
def getactivity(self, doraise: bool = True) -> Optional[ba.Activity]:
"""Return the ba.Activity this Actor is associated with.
If the Activity no longer exists, raises a ba.ActivityNotFoundError
or returns None depending on whether 'doraise' is set.
or returns None depending on whether 'doraise' is True.
"""
activity = self._activity()
if activity is None and doraise:
raise _error.ActivityNotFoundError()
raise ActivityNotFoundError()
return activity

View File

@ -0,0 +1,186 @@
# Released under the MIT License. See LICENSE for details.
#
"""Functionality related to ads."""
from __future__ import annotations
import time
from typing import TYPE_CHECKING
import _ba
if TYPE_CHECKING:
from typing import Optional, Callable, Any
class AdsSubsystem:
"""Subsystem for ads functionality in the app.
Category: App Classes
Access the single shared instance of this class at 'ba.app.ads'.
"""
def __init__(self) -> None:
self.last_ad_network = 'unknown'
self.last_ad_network_set_time = time.time()
self.ad_amt: Optional[float] = None
self.last_ad_purpose = 'invalid'
self.attempted_first_ad = False
self.last_in_game_ad_remove_message_show_time: Optional[float] = None
self.last_ad_completion_time: Optional[float] = None
self.last_ad_was_short = False
def do_remove_in_game_ads_message(self) -> None:
"""(internal)"""
from ba._language import Lstr
from ba._enums import TimeType
# Print this message once every 10 minutes at most.
tval = _ba.time(TimeType.REAL)
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(
1.0,
lambda: _ba.screenmessage(Lstr(
resource='removeInGameAdsText',
subs=[('${PRO}',
Lstr(resource='store.bombSquadProNameText')),
('${APP_NAME}', Lstr(resource='titleText'))]),
color=(1, 1, 0)),
timetype=TimeType.REAL)
def show_ad(self,
purpose: str,
on_completion_call: Callable[[], Any] = None) -> None:
"""(internal)"""
self.last_ad_purpose = purpose
_ba.show_ad(purpose, on_completion_call)
def show_ad_2(self,
purpose: str,
on_completion_call: Callable[[bool], Any] = None) -> None:
"""(internal)"""
self.last_ad_purpose = purpose
_ba.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._enums import TimeType
app = _ba.app
show = True
# No ads without net-connections, etc.
if not _ba.can_show_ad():
show = False
if app.accounts.have_pro():
show = False # Pro disables interstitials.
try:
session = _ba.get_foreground_host_session()
assert session is not None
is_tournament = session.tournament_id is not None
except Exception:
is_tournament = False
if is_tournament:
show = False # Never show ads during tournaments.
if show:
interval: Optional[float]
launch_count = app.config.get('launchCount', 0)
# If we're seeing short ads we may want to space them differently.
interval_mult = (_ba.get_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 = _ba.get_account_misc_read_val(
'ads.startVal1', 0.99)
else:
self.ad_amt = _ba.get_account_misc_read_val(
'ads.startVal2', 1.0)
interval = None
else:
# So far we're cleared to show; now calc our
# 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 = _ba.get_account_misc_read_val(base + '.minLC', 0.0)
max_lc = _ba.get_account_misc_read_val(base + '.maxLC', 5.0)
min_lc_scale = (_ba.get_account_misc_read_val(
base + '.minLCScale', 0.25))
max_lc_scale = (_ba.get_account_misc_read_val(
base + '.maxLCScale', 0.34))
min_lc_interval = (_ba.get_account_misc_read_val(
base + '.minLCInterval', 360))
max_lc_interval = (_ba.get_account_misc_read_val(
base + '.maxLCInterval', 300))
if launch_count < min_lc:
lc_amt = 0.0
elif launch_count > max_lc:
lc_amt = 1.0
else:
lc_amt = ((float(launch_count) - min_lc) /
(max_lc - min_lc))
incr = (1.0 - lc_amt) * min_lc_scale + lc_amt * max_lc_scale
interval = ((1.0 - lc_amt) * min_lc_interval +
lc_amt * max_lc_interval)
self.ad_amt += incr
assert self.ad_amt is not None
if self.ad_amt >= 1.0:
self.ad_amt = self.ad_amt % 1.0
self.attempted_first_ad = True
# After we've reached the traditional show-threshold once,
# try again whenever its been INTERVAL since our last successful
# show.
elif (
self.attempted_first_ad and
(self.last_ad_completion_time is None or
(interval is not None
and _ba.time(TimeType.REAL) - self.last_ad_completion_time >
(interval * interval_mult)))):
# Reset our other counter too in this case.
self.ad_amt = 0.0
else:
show = False
# If we're *still* cleared to show, actually tell the system to show.
if show:
# As a safety-check, set up an object that will run
# the completion callback if we've returned and sat for 10 seconds
# (in case some random ad network doesn't properly deliver its
# completion callback).
class _Payload:
def __init__(self, pcall: Callable[[], Any]):
self._call = pcall
self._ran = False
def run(self, fallback: bool = False) -> None:
"""Run fallback call (and issue a warning about it)."""
if not self._ran:
if fallback:
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))
_ba.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)
self.show_ad('between_game', on_completion_call=payload.run)
else:
_ba.pushcall(call) # Just run the callback without the ad.

View File

@ -0,0 +1,73 @@
# 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()

View File

@ -1,35 +1,23 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality related to the high level state of the app."""
from __future__ import annotations
import time
import random
from typing import TYPE_CHECKING
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._account import AccountSubsystem
from ba._meta import MetadataSubsystem
from ba._ads import AdsSubsystem
if TYPE_CHECKING:
import ba
from ba import _lang, _meta
from ba.ui import UICleanupCheck
from bastd.actor import spazappearance
from typing import Optional, Dict, Set, Any, Type, Tuple, Callable, List
@ -37,7 +25,7 @@ if TYPE_CHECKING:
class App:
"""A class for high level app functionality and state.
category: App Classes
Category: App Classes
Use ba.app to access the single shared instance of this class.
@ -46,11 +34,6 @@ class App:
"""
# pylint: disable=too-many-public-methods
# Note: many values here are simple method attrs and thus don't show
# up in docs. If there's any that'd be useful to expose publicly, they
# should be converted to properties so its possible to validate values
# and provide docs.
@property
def build_number(self) -> int:
"""Integer build number.
@ -58,96 +41,20 @@ class App:
This value increases by at least 1 with each release of the game.
It is independent of the human readable ba.App.version string.
"""
return self._build_number
assert isinstance(self._env['build_number'], int)
return self._env['build_number']
@property
def config_file_path(self) -> str:
"""Where the game's config file is stored on disk."""
return self._config_file_path
@property
def locale(self) -> str:
"""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
(which may differ from locale if the user sets a language, etc.)
"""
return self._locale
def can_display_language(self, language: str) -> bool:
"""Tell whether we can display a particular language.
(internal)
On some platforms we don't have unicode rendering yet
which limits the languages we can draw.
"""
# We don't yet support full unicode display on windows or linux :-(.
if (language in {
'Chinese', 'ChineseTraditional', 'Persian', 'Korean', 'Arabic',
'Hindi', 'Vietnamese'
} and self.platform in ('windows', 'linux')):
return False
return True
def _get_default_language(self) -> str:
languages = {
'de': 'German',
'es': 'Spanish',
'sk': 'Slovak',
'it': 'Italian',
'nl': 'Dutch',
'da': 'Danish',
'pt': 'Portuguese',
'fr': 'French',
'el': 'Greek',
'ru': 'Russian',
'pl': 'Polish',
'sv': 'Swedish',
'eo': 'Esperanto',
'cs': 'Czech',
'hr': 'Croatian',
'hu': 'Hungarian',
'be': 'Belarussian',
'ro': 'Romanian',
'ko': 'Korean',
'fa': 'Persian',
'ar': 'Arabic',
'zh': 'Chinese',
'tr': 'Turkish',
'id': 'Indonesian',
'sr': 'Serbian',
'uk': 'Ukrainian',
'vi': 'Vietnamese',
'hi': 'Hindi'
}
# Special case Chinese: specific variations map to traditional.
# (otherwise will map to 'Chinese' which is simplified)
if self.locale in ('zh_HANT', 'zh_TW'):
language = 'ChineseTraditional'
else:
language = languages.get(self.locale[:2], 'English')
if not self.can_display_language(language):
language = 'English'
return language
@property
def language(self) -> str:
"""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.
"""
assert isinstance(self.config, dict)
return self.config.get('Lang', self.default_language)
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."""
return self._user_agent_string
assert isinstance(self._env['user_agent_string'], str)
return self._env['user_agent_string']
@property
def version(self) -> str:
@ -157,7 +64,8 @@ class App:
string elements such as 'alpha', 'beta', 'test', etc.
If a numeric version is needed, use 'ba.App.build_number'.
"""
return self._version
assert isinstance(self._env['version'], str)
return self._env['version']
@property
def debug_build(self) -> bool:
@ -167,7 +75,8 @@ class App:
builds due to compiler optimizations being disabled and extra
checks being run.
"""
return self._debug_build
assert isinstance(self._env['debug_build'], bool)
return self._env['debug_build']
@property
def test_build(self) -> bool:
@ -176,22 +85,26 @@ class App:
Test mode enables extra checks and features that are useful for
release testing but which do not slow the game down significantly.
"""
return self._test_build
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."""
return self._python_directory_user
assert isinstance(self._env['python_directory_user'], str)
return self._env['python_directory_user']
@property
def python_directory_ba(self) -> str:
def python_directory_app(self) -> str:
"""Path where the app looks for its bundled scripts."""
return self._python_directory_ba
assert isinstance(self._env['python_directory_app'], str)
return self._env['python_directory_app']
@property
def python_directory_site(self) -> str:
def python_directory_app_site(self) -> str:
"""Path containing pip packages bundled with the app."""
return self._python_directory_site
assert isinstance(self._env['python_directory_app_site'], str)
return self._env['python_directory_app_site']
@property
def config(self) -> ba.AppConfig:
@ -205,7 +118,8 @@ class App:
Examples are: 'mac', 'windows', android'.
"""
return self._platform
assert isinstance(self._env['platform'], str)
return self._env['platform']
@property
def subplatform(self) -> str:
@ -214,43 +128,33 @@ class App:
Can be empty. For the 'android' platform, subplatform may
be 'google', 'amazon', etc.
"""
return self._subplatform
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 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.
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 interface_type(self) -> str:
"""Interface mode the game is in; can be 'large', 'medium', or 'small'.
'large' is used by system such as desktop PC where elements on screen
remain usable even at small sizes, allowing more to be shown.
'small' is used by small devices such as phones, where elements on
screen must be larger to remain readable and usable.
'medium' is used by tablets and other middle-of-the-road situations
such as VR or TV.
"""
return self._interface_type
@property
def on_tv(self) -> bool:
"""Bool value for if the game is running on a TV."""
return self._on_tv
"""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:
"""Bool value for if the game is running in VR."""
return self._vr_mode
"""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]:
@ -267,7 +171,6 @@ class App:
the single shared instance.
"""
# pylint: disable=too-many-statements
from ba._music import MusicController
# Config.
self.config_file_healthy = False
@ -277,68 +180,31 @@ class App:
# refreshed/etc.
self.fg_state = 0
# Environment stuff.
# (pulling these into attrs so we can type-check them and provide docs)
env = _ba.env()
self._build_number: int = env['build_number']
assert isinstance(self._build_number, int)
self._config_file_path: str = env['config_file_path']
assert isinstance(self._config_file_path, str)
self._locale: str = env['locale']
assert isinstance(self._locale, str)
self._user_agent_string: str = env['user_agent_string']
assert isinstance(self._user_agent_string, str)
self._version: str = env['version']
assert isinstance(self._version, str)
self._debug_build: bool = env['debug_build']
assert isinstance(self._debug_build, bool)
self._test_build: bool = env['test_build']
assert isinstance(self._test_build, bool)
self._python_directory_user: str = env['python_directory_user']
assert isinstance(self._python_directory_user, str)
self._python_directory_ba: str = env['python_directory_ba']
assert isinstance(self._python_directory_ba, str)
self._python_directory_site: str = env['python_directory_site']
assert isinstance(self._python_directory_site, str)
self._platform: str = env['platform']
assert isinstance(self._platform, str)
self._subplatform: str = env['subplatform']
assert isinstance(self._subplatform, str)
self._interface_type: str = env['interface_type']
assert isinstance(self._interface_type, str)
self._on_tv: bool = env['on_tv']
assert isinstance(self._on_tv, bool)
self._vr_mode: bool = env['vr_mode']
assert isinstance(self._vr_mode, bool)
self.protocol_version: int = env['protocol_version']
self._env = _ba.env()
self.protocol_version: int = self._env['protocol_version']
assert isinstance(self.protocol_version, int)
self.toolbar_test: bool = env['toolbar_test']
self.toolbar_test: bool = self._env['toolbar_test']
assert isinstance(self.toolbar_test, bool)
self.kiosk_mode: bool = env['kiosk_mode']
assert isinstance(self.kiosk_mode, bool)
self.headless_build: bool = env['headless_build']
assert isinstance(self.headless_build, 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
# Misc.
self.default_language = self._get_default_language()
self.metascan: Optional[_meta.ScanResults] = None
self.tips: List[str] = []
self.stress_test_reset_timer: Optional[ba.Timer] = None
self.suppress_debug_reports = False
self.last_ad_completion_time: Optional[float] = None
self.last_ad_was_short = False
self.did_weak_call_warning = False
self.ran_on_app_launch = False
# If we try to run promo-codes due to launch-args/etc we might
# not be signed in yet; go ahead and queue them up in that case.
self.pending_promo_codes: List[str] = []
self.last_in_game_ad_remove_message_show_time: Optional[float] = None
self.log_have_new = False
self.log_upload_timer_started = False
self._config: Optional[ba.AppConfig] = None
self.printed_live_object_warning = False
self.last_post_purchase_message_time: Optional[float] = None
# We include this extra hash with shared input-mapping names so
# that we don't share mappings between differently-configured
@ -353,30 +219,18 @@ class App:
# Server Mode.
self.server: Optional[ba.ServerController] = None
# Ads.
self.last_ad_network = 'unknown'
self.last_ad_network_set_time = time.time()
self.ad_amt: Optional[float] = None
self.last_ad_purpose = 'invalid'
self.attempted_first_ad = False
# Music.
self.music = MusicController()
# Language.
self.language_target: Optional[_lang.AttrDict] = None
self.language_merged: Optional[_lang.AttrDict] = None
# Achievements.
self.achievements: List[ba.Achievement] = []
self.achievements_to_display: (List[Tuple[ba.Achievement, bool]]) = []
self.achievement_display_timer: Optional[_ba.Timer] = None
self.last_achievement_display_time: float = 0.0
self.achievement_completion_banner_slots: Set[int] = set()
self.meta = MetadataSubsystem()
self.accounts = AccountSubsystem()
self.plugins = PluginSubsystem()
self.music = MusicSubsystem()
self.lang = LanguageSubsystem()
self.ach = AchievementSubsystem()
self.ui = UISubsystem()
self.ads = AdsSubsystem()
# Lobby.
self.lobby_random_profile_index: int = 1
self.lobby_random_char_index_offset: Optional[int] = None
self.lobby_random_char_index_offset = random.randrange(1000)
self.lobby_account_profile_device_id: Optional[int] = None
# Main Menu.
@ -395,61 +249,31 @@ class App:
self.ffa_series_length = 24
self.coop_session_args: Dict = {}
# UI.
self.uicontroller: Optional[ba.UIController] = None
self.main_menu_window: Optional[_ba.Widget] = None # FIXME: Kill this.
self.window_states: Dict = {} # FIXME: Kill this.
self.windows: Dict = {} # FIXME: Kill this.
self.main_window: Optional[str] = None # FIXME: Kill this.
self.main_menu_selection: Optional[str] = None # FIXME: Kill this.
self.have_party_queue_window = False
self.quit_window: Any = None
self.dismiss_wii_remotes_window_call: (Optional[Callable[[],
Any]]) = None
self.value_test_defaults: dict = {}
self.main_menu_window_refresh_check_count = 0
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: Optional[Dict] = None
self.league_rank_cache: Dict = {}
self.tournament_info: Dict = {}
self.account_tournament_list: Optional[Tuple[int, List[str]]] = None
self.ping_thread_count = 0
self.invite_confirm_windows: List[Any] = [] # FIXME: Don't use Any.
self.store_layout: Optional[Dict[str, List[Dict[str, Any]]]] = None
self.store_items: Optional[Dict[str, Dict]] = None
self.pro_sale_start_time: Optional[int] = None
self.pro_sale_start_val: Optional[int] = None
self.party_window: Any = None # FIXME: Don't use Any.
self.title_color = (0.72, 0.7, 0.75)
self.heading_color = (0.72, 0.7, 0.75)
self.infotextcolor = (0.7, 0.9, 0.7)
self.uicleanupchecks: List[UICleanupCheck] = []
self.uiupkeeptimer: Optional[ba.Timer] = None
self.delegate: Optional[ba.AppDelegate] = None
# A few shortcuts.
self.small_ui = env['interface_type'] == 'small'
self.med_ui = env['interface_type'] == 'medium'
self.large_ui = env['interface_type'] == 'large'
self.toolbars = env.get('toolbar_test', True)
def on_app_launch(self) -> None:
"""Runs after the app finishes bootstrapping.
(internal)"""
# FIXME: Break this up.
# pylint: disable=too-many-statements
# pylint: disable=too-many-locals
# pylint: disable=cyclic-import
from ba import _apputils
from ba import _appconfig
from ba.ui import UIController, ui_upkeep
from ba import _achievement
from ba import _map
from ba import _meta
from ba import _campaign
from bastd import appdelegate
from bastd import maps as stdmaps
@ -460,8 +284,8 @@ class App:
self.delegate = appdelegate.AppDelegate()
self.uicontroller = UIController()
_achievement.init_achievements()
self.ui.on_app_launch()
spazappearance.register_appearances()
_campaign.init_campaigns()
@ -476,58 +300,15 @@ class App:
]:
_map.register_map(maptype)
if self.debug_build:
_apputils.suppress_debug_reports()
# 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 _ba.is_blessed()):
_ba.screenmessage('WARNING: NON-BLESSED BUILD', color=(1, 0, 0))
# IMPORTANT - if tweaking UI stuff, you need to make sure it behaves
# for small, medium, and large UI modes. (doesn't run off screen, etc).
# Set these to 1 to test with different sizes. Generally small is used
# on phones, medium is used on tablets, and large is on desktops or
# large tablets.
# Kick off our periodic UI upkeep.
# FIXME: Can probably kill this if we do immediate UI death checks.
self.uiupkeeptimer = _ba.Timer(2.6543,
ui_upkeep,
timetype=TimeType.REAL,
repeat=True)
if bool(False): # force-test small UI
self.small_ui = True
self.med_ui = False
with _ba.Context('ui'):
_ba.pushcall(lambda: _ba.screenmessage(
'FORCING SMALL UI FOR TESTING', color=(1, 0, 1), log=True))
if bool(False): # force-test medium UI
self.small_ui = False
self.med_ui = True
with _ba.Context('ui'):
_ba.pushcall(lambda: _ba.screenmessage(
'FORCING MEDIUM UI FOR TESTING', color=(1, 0, 1
), log=True))
if bool(False): # force-test large UI
self.small_ui = False
self.med_ui = False
with _ba.Context('ui'):
_ba.pushcall(lambda: _ba.screenmessage(
'FORCING LARGE UI FOR TESTING', color=(1, 0, 1), log=True))
# If there's a leftover log file, attempt to upload
# it to the server and/or get rid of it.
# If there's a leftover log file, attempt to upload it to the
# master-server and/or get rid of it.
_apputils.handle_leftover_log_file()
try:
_apputils.handle_leftover_log_file()
except Exception:
from ba import _error
_error.print_exception('Error handling leftover log file')
# Notify the user if we're using custom system scripts.
# FIXME: This no longer works since sys-scripts is an absolute path;
# need to just add a proper call to query this.
# if env['python_directory_ba'] != 'data/scripts':
# ba.screenmessage("Using custom system scripts...",
# color=(0, 1, 0))
# Only do this stuff if our config file is healthy so we don't
# overwrite a broken one or whatnot and wipe out data.
@ -554,21 +335,24 @@ class App:
# Debugging - make note if we're using the local test server so we
# don't accidentally leave it on in a release.
# FIXME - move this to native layer.
# FIXME - should move this to the native layer.
server_addr = _ba.get_master_server_address()
if 'localhost' in server_addr:
_ba.timer(2.0,
lambda: _ba.screenmessage('Note: using local server',
(1, 1, 0),
log=True),
lambda: _ba.screenmessage(
'Note: using local server',
(1, 1, 0),
log=True,
),
timetype=TimeType.REAL)
elif 'test' in server_addr:
_ba.timer(
2.0,
lambda: _ba.screenmessage('Note: using test server-module',
(1, 1, 0),
log=True),
timetype=TimeType.REAL)
_ba.timer(2.0,
lambda: _ba.screenmessage(
'Note: using test server-module',
(1, 1, 0),
log=True,
),
timetype=TimeType.REAL)
cfg['launchCount'] = launch_count
cfg.commit()
@ -576,27 +360,19 @@ class App:
# 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 import specialoffer
from bastd.ui.specialoffer import show_offer
config = self.config
if ('pendingSpecialOffer' in config and _ba.get_public_login_id()
== config['pendingSpecialOffer']['a']):
self.special_offer = config['pendingSpecialOffer']['o']
specialoffer.show_offer()
show_offer()
if not self.headless_build:
if not self.headless_mode:
_ba.timer(3.0, check_special_offer, timetype=TimeType.REAL)
# Start scanning for things exposed via ba_meta.
_meta.start_scan()
# Auto-sign-in to a local account in a moment if we're set to.
def do_auto_sign_in() -> None:
if self.headless_build:
_ba.sign_in('Local')
elif cfg.get('Auto Account State') == 'Local':
_ba.sign_in('Local')
_ba.pushcall(do_auto_sign_in)
self.meta.on_app_launch()
self.accounts.on_app_launch()
self.plugins.on_app_launch()
self.ran_on_app_launch = True
@ -617,26 +393,27 @@ class App:
activity: Optional[ba.Activity] = _ba.get_foreground_host_activity()
if (activity is not None and activity.allow_pausing
and not _ba.have_connected_clients()):
from ba import _gameutils, _lang
from ba import _gameutils
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 = _gameutils.sharedobj('globals')
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': _lang.Lstr(resource='pausedByHostText'),
'client_only': True,
'flatness': 1.0,
'h_align': 'center'
}))
_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.
@ -644,14 +421,13 @@ class App:
If there's a foreground host-activity that's currently paused, tell it
to resume.
"""
from ba._gameutils import sharedobj
# 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 = sharedobj('globals')
globs = activity.globalsnode
if globs.paused:
_ba.playsound(_ba.getsound('refWhistle'))
globs.paused = False
@ -659,13 +435,16 @@ class App:
# FIXME: This should not be an actor attr.
activity.paused_text = None
def return_to_main_menu_session_gracefully(self) -> None:
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
_ba.app.main_window = None
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.
@ -697,7 +476,7 @@ class App:
"""(internal)"""
# If there's no main menu up, just call immediately.
if not self.main_menu_window:
if not self.ui.has_main_menu_window():
with _ba.Context('ui'):
call()
else:
@ -709,41 +488,35 @@ class App:
def on_app_resume(self) -> None:
"""Run when the app resumes from a suspended state."""
self.music.on_app_resume()
self.fg_state += 1
# Mark our cached tourneys as invalid so anyone using them knows
# they might be out of date.
for entry in list(self.tournament_info.values()):
entry['valid'] = False
self.accounts.on_app_resume()
self.music.on_app_resume()
def launch_coop_game(self,
game: str,
force: bool = False,
args: Dict = None) -> bool:
"""High level way to launch a co-op session locally."""
"""High level way to launch a local co-op session."""
# pylint: disable=cyclic-import
from ba._campaign import get_campaign
from ba._campaign import getcampaign
from bastd.ui.coop.level import CoopLevelLockedWindow
if args is None:
args = {}
if game == '':
raise Exception('empty game name')
raise ValueError('empty game name')
campaignname, levelname = game.split(':')
campaign = get_campaign(campaignname)
levels = campaign.get_levels()
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 levels:
for level in campaign.levels:
if level.name == levelname:
break
if not level.complete:
CoopLevelLockedWindow(
campaign.get_level(levelname).displayname,
campaign.get_level(level.name).displayname)
campaign.getlevel(levelname).displayname,
campaign.getlevel(level.name).displayname)
return False
# Ok, we're good to go.
@ -767,65 +540,17 @@ class App:
_ba.fade_screen(False, endcall=_fade_end)
return True
def do_remove_in_game_ads_message(self) -> None:
"""(internal)"""
from ba._lang import Lstr
from ba._enums import TimeType
# Print this message once every 10 minutes at most.
tval = _ba.time(TimeType.REAL)
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(
1.0,
lambda: _ba.screenmessage(Lstr(
resource='removeInGameAdsText',
subs=[('${PRO}',
Lstr(resource='store.bombSquadProNameText')),
('${APP_NAME}', Lstr(resource='titleText'))]),
color=(1, 1, 0)),
timetype=TimeType.REAL)
def shutdown(self) -> None:
def on_app_shutdown(self) -> None:
"""(internal)"""
self.music.on_app_shutdown()
def handle_deep_link(self, url: str) -> None:
"""Handle a deep link URL."""
from ba._lang import Lstr
from ba._enums import TimeType
if url.startswith('ballisticacore://code/'):
code = url.replace('ballisticacore://code/', '')
# 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 _ba.get_account_state() != 'signed_in':
def check_pending_codes() -> None:
"""(internal)"""
# If we're still not signed in and have pending codes,
# inform the user that they need to sign in to use them.
if _ba.app.pending_promo_codes:
_ba.screenmessage(
Lstr(resource='signInForPromoCodeText'),
color=(1, 0, 0))
_ba.playsound(_ba.getsound('error'))
_ba.app.pending_promo_codes.append(code)
_ba.timer(6.0, check_pending_codes, timetype=TimeType.REAL)
return
_ba.screenmessage(Lstr(resource='submittingPromoCodeText'),
color=(0, 1, 0))
_ba.add_transaction({
'type': 'PROMO_CODE',
'expire_time': time.time() + 5,
'code': code
})
_ba.run_transactions()
from ba._language import Lstr
appname = _ba.appname()
if url.startswith(f'{appname}://code/'):
code = url.replace(f'{appname}://code/', '')
self.accounts.add_pending_promo_code(code)
else:
_ba.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
_ba.playsound(_ba.getsound('error'))
@ -833,7 +558,7 @@ class App:
def _test_https(self) -> None:
"""Testing https support.
(would be nice to get this working on our custom python builds; need
(would be nice to get this working on our custom Python builds; need
to wrangle certificates somehow).
"""
import urllib.request

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Provides the AppConfig class."""
from __future__ import annotations
@ -42,7 +24,7 @@ class AppConfig(dict):
AppConfig data is stored as json on disk on so make sure to only place
json-friendly values in it (dict, list, str, float, int, bool).
Be aware that tuples will be quietly converted to lists.
Be aware that tuples will be quietly converted to lists when stored.
"""
def resolve(self, key: str) -> Any:
@ -149,7 +131,7 @@ def read_config() -> Tuple[AppConfig, bool]:
try:
_ba.log('broken config contents:\n' +
config_contents.replace('\000', '<NULL_BYTE>'),
to_console=False)
to_stdout=False)
except Exception as exc:
print('EXC logging broken config contents:', exc)
config = AppConfig()

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Defines AppDelegate class for handling high level app functionality."""
from __future__ import annotations
@ -34,17 +16,16 @@ class AppDelegate:
Category: App Classes
"""
def create_default_game_config_ui(
def create_default_game_settings_ui(
self, gameclass: Type[ba.GameActivity],
sessionclass: Type[ba.Session], config: Optional[Dict[str, Any]],
completion_call: Callable[[Optional[Dict[str, Any]]],
None]) -> None:
sessiontype: Type[ba.Session], settings: Optional[dict],
completion_call: Callable[[Optional[dict]], None]) -> None:
"""Launch a UI to configure the given game config.
It should manipulate the contents of config and call completion_call
when done.
"""
del gameclass, sessionclass, config, completion_call # unused
del gameclass, sessiontype, settings, completion_call # Unused.
from ba import _error
_error.print_error(
"create_default_game_config_ui needs to be overridden")
"create_default_game_settings_ui needs to be overridden")

View File

@ -0,0 +1,3 @@
# Released under the MIT License. See LICENSE for details.
#
"""Functionality related to the high level state of the app."""

View File

@ -1,26 +1,9 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Utility functionality related to the overall operation of the app."""
from __future__ import annotations
import gc
import os
from typing import TYPE_CHECKING
@ -43,7 +26,7 @@ def is_browser_likely_available() -> bool:
"""
app = _ba.app
platform = app.platform
touchscreen = _ba.get_input_device('TouchScreen', '#1', doraise=False)
touchscreen = _ba.getinputdevice('TouchScreen', '#1', doraise=False)
# If we're on a vr device or an android device with no touchscreen,
# assume no browser.
@ -58,8 +41,8 @@ def is_browser_likely_available() -> bool:
def get_remote_app_name() -> ba.Lstr:
"""(internal)"""
from ba import _lang
return _lang.Lstr(resource='remote_app.app_name')
from ba import _language
return _language.Lstr(resource='remote_app.app_name')
def should_submit_debug_info() -> bool:
@ -67,60 +50,51 @@ def should_submit_debug_info() -> bool:
return _ba.app.config.get('Submit Debug Info', True)
def suppress_debug_reports() -> None:
"""Turn debug-reporting to the master server off.
This should be called in devel/debug situations to avoid spamming
the master server with spurious logs.
"""
# _ba.screenmessage("Suppressing debug reports.", color=(1, 0, 0))
_ba.app.suppress_debug_reports = True
def handle_log() -> None:
"""Called on debug log prints.
When this happens, we can upload our log to the server
after a short bit if desired.
"""
from ba._netutils import serverput
from ba._netutils import master_server_post
from ba._enums import TimeType
app = _ba.app
app.log_have_new = True
if not app.log_upload_timer_started:
def _put_log() -> None:
if not app.suppress_debug_reports:
try:
sessionname = str(_ba.get_foreground_host_session())
except Exception:
sessionname = 'unavailable'
try:
activityname = str(_ba.get_foreground_host_activity())
except Exception:
activityname = 'unavailable'
info = {
'log': _ba.get_log(),
'version': app.version,
'build': app.build_number,
'userAgentString': app.user_agent_string,
'session': sessionname,
'activity': activityname,
'fatal': 0,
'userRanCommands': _ba.has_user_run_commands(),
'time': _ba.time(TimeType.REAL),
'userModded': _ba.has_user_mods()
}
try:
sessionname = str(_ba.get_foreground_host_session())
except Exception:
sessionname = 'unavailable'
try:
activityname = str(_ba.get_foreground_host_activity())
except Exception:
activityname = 'unavailable'
def response(data: Any) -> 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()
info = {
'log': _ba.getlog(),
'version': app.version,
'build': app.build_number,
'userAgentString': app.user_agent_string,
'session': sessionname,
'activity': activityname,
'fatal': 0,
'userRanCommands': _ba.has_user_run_commands(),
'time': _ba.time(TimeType.REAL),
'userModded': _ba.has_user_mods(),
'newsShow': _ba.get_news_show(),
}
serverput('bsLog', info, response)
def response(data: Any) -> 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()
master_server_post('bsLog', info, response)
app.log_upload_timer_started = True
@ -145,31 +119,40 @@ def handle_log() -> None:
def handle_leftover_log_file() -> None:
"""Handle an un-uploaded log from a previous run."""
import json
from ba._netutils import serverput
try:
import json
from ba._netutils import master_server_post
if os.path.exists(_ba.get_log_file_path()):
with open(_ba.get_log_file_path()) as infile:
info = json.loads(infile.read())
infile.close()
do_send = should_submit_debug_info()
if do_send:
if os.path.exists(_ba.get_log_file_path()):
with open(_ba.get_log_file_path()) as infile:
info = json.loads(infile.read())
infile.close()
do_send = should_submit_debug_info()
if do_send:
def response(data: Any) -> None:
# Non-None response means we were successful;
# lets kill it.
if data is not None:
os.remove(_ba.get_log_file_path())
def response(data: Any) -> None:
# Non-None response means we were successful;
# lets kill it.
if data is not None:
try:
os.remove(_ba.get_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
serverput('bsLog', info, response)
else:
# If they don't want logs uploaded just kill it.
os.remove(_ba.get_log_file_path())
master_server_post('bsLog', info, response)
else:
# If they don't want logs uploaded just kill it.
os.remove(_ba.get_log_file_path())
except Exception:
from ba import _error
_error.print_exception('Error handling leftover log file.')
def garbage_collect(session_end: bool = True) -> None:
"""Run an explicit pass of garbage collection."""
import gc
def garbage_collect_session_end() -> None:
"""Run explicit garbage collection with extra checks for session end."""
gc.collect()
# Can be handy to print this to check for leaks between games.
@ -179,8 +162,19 @@ def garbage_collect(session_end: bool = True) -> None:
print('PYTHON GC FOUND', len(gc.garbage), 'UNCOLLECTIBLE OBJECTS:')
for i, obj in enumerate(gc.garbage):
print(str(i) + ':', obj)
if session_end:
print_live_object_warnings('after session shutdown')
print_live_object_warnings('after session shutdown')
def garbage_collect() -> None:
"""Run an explicit pass of garbage collection.
category: General Utility Functions
May also print warnings/etc. if collection takes too long or if
uncollectible objects are found (so use this instead of simply
gc.collect().
"""
gc.collect()
def print_live_object_warnings(when: Any,
@ -188,22 +182,24 @@ def print_live_object_warnings(when: Any,
ignore_activity: ba.Activity = None) -> None:
"""Print warnings for remaining objects in the current context."""
# pylint: disable=cyclic-import
import gc
from ba import _session as bs_session
from ba import _actor as bs_actor
from ba import _activity as bs_activity
from ba._session import Session
from ba._actor import Actor
from ba._activity import Activity
sessions: List[ba.Session] = []
activities: List[ba.Activity] = []
actors = []
actors: List[ba.Actor] = []
# Once we come across leaked stuff, printing again is probably
# redundant.
if _ba.app.printed_live_object_warning:
# print 'skipping live obj check due to previous found live object(s)'
return
for obj in gc.get_objects():
if isinstance(obj, bs_actor.Actor):
if isinstance(obj, Actor):
actors.append(obj)
elif isinstance(obj, bs_session.Session):
elif isinstance(obj, Session):
sessions.append(obj)
elif isinstance(obj, bs_activity.Activity):
elif isinstance(obj, Activity):
activities.append(obj)
# Complain about any remaining sessions.
@ -211,210 +207,32 @@ def print_live_object_warnings(when: Any,
if session is ignore_session:
continue
_ba.app.printed_live_object_warning = True
print('ERROR: Session found', when, ':', session)
# refs = list(gc.get_referrers(session))
# i = 1
# for ref in refs:
# if type(ref) is types.FrameType: continue
# print ' ref', i, ':', ref
# i += 1
# if type(ref) is list or type(ref) is tuple or type(ref) is dict:
# refs2 = list(gc.get_referrers(ref))
# j = 1
# for ref2 in refs2:
# if type(ref2) is types.FrameType: continue
# print ' ref\'s ref', j, ':', ref2
# j += 1
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('ERROR: Activity found', when, ':', activity)
# refs = list(gc.get_referrers(activity))
# i = 1
# for ref in refs:
# if type(ref) is types.FrameType: continue
# print ' ref', i, ':', ref
# i += 1
# if type(ref) is list or type(ref) is tuple or type(ref) is dict:
# refs2 = list(gc.get_referrers(ref))
# j = 1
# for ref2 in refs2:
# if type(ref2) is types.FrameType: continue
# print ' ref\'s ref', j, ':', ref2
# j += 1
print(f'ERROR: Activity found {when}: {activity}')
# Complain about any remaining actors.
for actor in actors:
_ba.app.printed_live_object_warning = True
print('ERROR: Actor found', when, ':', actor)
# if isinstance(actor, bs_actor.Actor):
# try:
# if actor.node:
# print(' - contains node:',
# actor.node.getnodetype(), ';',
# actor.node.get_name())
# except Exception as exc:
# print(' - exception checking actor node:', exc)
# refs = list(gc.get_referrers(actor))
# i = 1
# for ref in refs:
# if type(ref) is types.FrameType: continue
# print ' ref', i, ':', ref
# i += 1
# if type(ref) is list or type(ref) is tuple or type(ref) is dict:
# refs2 = list(gc.get_referrers(ref))
# j = 1
# for ref2 in refs2:
# if type(ref2) is types.FrameType: continue
# print ' ref\'s ref', j, ':', ref2
# j += 1
print(f'ERROR: Actor found {when}: {actor}')
def print_corrupt_file_error() -> None:
"""Print an error if a corrupt file is found."""
from ba._lang import get_resource
from ba._general import Call
from ba._enums import TimeType
_ba.timer(
2.0,
lambda: _ba.screenmessage(get_resource('internal.corruptFileText').
replace('${EMAIL}', 'support@froemling.net'),
color=(1, 0, 0)),
timetype=TimeType.REAL)
_ba.timer(2.0,
lambda: _ba.screenmessage(
_ba.app.lang.get_resource('internal.corruptFileText').
replace('${EMAIL}', 'support@froemling.net'),
color=(1, 0, 0),
),
timetype=TimeType.REAL)
_ba.timer(2.0,
Call(_ba.playsound, _ba.getsound('error')),
timetype=TimeType.REAL)
def show_ad(purpose: str,
on_completion_call: Callable[[], Any] = None) -> None:
"""(internal)"""
_ba.app.last_ad_purpose = purpose
_ba.show_ad(purpose, on_completion_call)
def show_ad_2(purpose: str,
on_completion_call: Callable[[bool], Any] = None) -> None:
"""(internal)"""
_ba.app.last_ad_purpose = purpose
_ba.show_ad_2(purpose, on_completion_call)
def call_after_ad(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._account import have_pro
from ba._enums import TimeType
import time
app = _ba.app
show = True
# No ads without net-connections, etc.
if not _ba.can_show_ad():
show = False
if have_pro():
show = False # Pro disables interstitials.
try:
session = _ba.get_foreground_host_session()
assert session is not None
is_tournament = session.tournament_id is not None
except Exception:
is_tournament = False
if is_tournament:
show = False # Never show ads during tournaments.
if show:
interval: Optional[float]
launch_count = app.config.get('launchCount', 0)
# If we're seeing short ads we may want to space them differently.
interval_mult = (_ba.get_account_misc_read_val(
'ads.shortIntervalMult', 1.0) if app.last_ad_was_short else 1.0)
if app.ad_amt is None:
if launch_count <= 1:
app.ad_amt = _ba.get_account_misc_read_val(
'ads.startVal1', 0.99)
else:
app.ad_amt = _ba.get_account_misc_read_val(
'ads.startVal2', 1.0)
interval = None
else:
# So far we're cleared to show; now calc our 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 = _ba.get_account_misc_read_val(base + '.minLC', 0.0)
max_lc = _ba.get_account_misc_read_val(base + '.maxLC', 5.0)
min_lc_scale = (_ba.get_account_misc_read_val(
base + '.minLCScale', 0.25))
max_lc_scale = (_ba.get_account_misc_read_val(
base + '.maxLCScale', 0.34))
min_lc_interval = (_ba.get_account_misc_read_val(
base + '.minLCInterval', 360))
max_lc_interval = (_ba.get_account_misc_read_val(
base + '.maxLCInterval', 300))
if launch_count < min_lc:
lc_amt = 0.0
elif launch_count > max_lc:
lc_amt = 1.0
else:
lc_amt = ((float(launch_count) - min_lc) / (max_lc - min_lc))
incr = (1.0 - lc_amt) * min_lc_scale + lc_amt * max_lc_scale
interval = ((1.0 - lc_amt) * min_lc_interval +
lc_amt * max_lc_interval)
app.ad_amt += incr
assert app.ad_amt is not None
if app.ad_amt >= 1.0:
app.ad_amt = app.ad_amt % 1.0
app.attempted_first_ad = True
# After we've reached the traditional show-threshold once,
# try again whenever its been INTERVAL since our last successful show.
elif (app.attempted_first_ad
and (app.last_ad_completion_time is None or
(interval is not None
and _ba.time(TimeType.REAL) - app.last_ad_completion_time >
(interval * interval_mult)))):
# Reset our other counter too in this case.
app.ad_amt = 0.0
else:
show = False
# If we're *still* cleared to show, actually tell the system to show.
if show:
# As a safety-check, set up an object that will run
# the completion callback if we've returned and sat for 10 seconds
# (in case some random ad network doesn't properly deliver its
# completion callback).
class _Payload:
def __init__(self, pcall: Callable[[], Any]):
self._call = pcall
self._ran = False
def run(self, fallback: bool = False) -> None:
"""Run the fallback call (and issues a warning about it)."""
if not self._ran:
if fallback:
print((
'ERROR: relying on fallback ad-callback! '
'last network: ' + app.last_ad_network + ' (set ' +
str(int(time.time() -
app.last_ad_network_set_time)) +
's ago); purpose=' + app.last_ad_purpose))
_ba.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)
show_ad('between_game', on_completion_call=payload.run)
else:
_ba.pushcall(call) # Just run the callback without the ad.

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality related to managing cloud based assets."""
from __future__ import annotations

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Benchmark/Stress-Test related functionality."""
from __future__ import annotations
@ -42,7 +24,7 @@ def run_cpu_benchmark() -> None:
def __init__(self) -> None:
print('FIXME: BENCHMARK SESSION WOULD CALC DEPS.')
# print('FIXME: BENCHMARK SESSION WOULD CALC DEPS.')
depsets: Sequence[ba.DependencySet] = []
super().__init__(depsets)
@ -53,7 +35,7 @@ def run_cpu_benchmark() -> None:
cfg['Graphics Quality'] = 'Low'
cfg.apply()
self.benchmark_type = 'cpu'
self.set_activity(_ba.new_activity(tutorial.TutorialActivity))
self.setactivity(_ba.newactivity(tutorial.TutorialActivity))
def __del__(self) -> None:
@ -62,7 +44,7 @@ def run_cpu_benchmark() -> None:
cfg['Graphics Quality'] = self._old_quality
cfg.apply()
def on_player_request(self, player: ba.Player) -> bool:
def on_player_request(self, player: ba.SessionPlayer) -> bool:
return False
_ba.new_host_session(BenchmarkSession, benchmark_type='cpu')
@ -73,12 +55,12 @@ def run_stress_test(playlist_type: str = 'Random',
player_count: int = 8,
round_duration: int = 30) -> None:
"""Run a stress test."""
from ba import _modutils
from ba import modutils
from ba._general import Call
from ba._enums import TimeType
_ba.screenmessage(
'Beginning stress test.. use '
'\'End Game\' to stop testing.',
"'End Game' to stop testing.",
color=(1, 1, 0))
with _ba.Context('ui'):
start_stress_test({
@ -90,8 +72,8 @@ def run_stress_test(playlist_type: str = 'Random',
_ba.timer(7.0,
Call(_ba.screenmessage,
('stats will be written to ' +
_modutils.get_human_readable_user_scripts_path() +
'/stressTestStats.csv')),
modutils.get_human_readable_user_scripts_path() +
'/stress_test_stats.csv')),
timetype=TimeType.REAL)
@ -112,7 +94,7 @@ def start_stress_test(args: Dict[str, Any]) -> None:
from ba._dualteamsession import DualTeamSession
from ba._freeforallsession import FreeForAllSession
from ba._enums import TimeType, TimeFormat
bs_config = _ba.app.config
appconfig = _ba.app.config
playlist_type = args['playlist_type']
if playlist_type == 'Random':
if random.random() < 0.5:
@ -122,15 +104,15 @@ def start_stress_test(args: Dict[str, Any]) -> None:
_ba.screenmessage('Running Stress Test (listType="' + playlist_type +
'", listName="' + args['playlist_name'] + '")...')
if playlist_type == 'Teams':
bs_config['Team Tournament Playlist Selection'] = args['playlist_name']
bs_config['Team Tournament Playlist Randomize'] = 1
appconfig['Team Tournament Playlist Selection'] = args['playlist_name']
appconfig['Team Tournament Playlist Randomize'] = 1
_ba.timer(1.0,
Call(_ba.pushcall, Call(_ba.new_host_session,
DualTeamSession)),
timetype=TimeType.REAL)
else:
bs_config['Free-for-All Playlist Selection'] = args['playlist_name']
bs_config['Free-for-All Playlist Randomize'] = 1
appconfig['Free-for-All Playlist Selection'] = args['playlist_name']
appconfig['Free-for-All Playlist Randomize'] = 1
_ba.timer(1.0,
Call(_ba.pushcall,
Call(_ba.new_host_session, FreeForAllSession)),
@ -169,13 +151,14 @@ def run_media_reload_benchmark() -> None:
def delay_add(start_time: float) -> None:
def doit(start_time_2: float) -> None:
from ba import _lang
_ba.screenmessage(
_lang.get_resource('debugWindow.totalReloadTimeText').replace(
'${TIME}', str(_ba.time(TimeType.REAL) - start_time_2)))
_ba.app.lang.get_resource(
'debugWindow.totalReloadTimeText').replace(
'${TIME}',
str(_ba.time(TimeType.REAL) - start_time_2)))
_ba.print_load_info()
if _ba.app.config.resolve('Texture Quality') != 'High':
_ba.screenmessage(_lang.get_resource(
_ba.screenmessage(_ba.app.lang.get_resource(
'debugWindow.reloadBenchmarkBestResultsText'),
color=(1, 1, 0))

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality related to co-op campaigns."""
from __future__ import annotations
@ -35,7 +17,7 @@ def register_campaign(campaign: ba.Campaign) -> None:
_ba.app.campaigns[campaign.name] = campaign
def get_campaign(name: str) -> ba.Campaign:
def getcampaign(name: str) -> ba.Campaign:
"""Return a campaign by name."""
return _ba.app.campaigns[name]
@ -61,24 +43,27 @@ class Campaign:
"""Whether this Campaign's levels must be played in sequence."""
return self._sequential
def add_level(self, level: ba.Level) -> None:
def addlevel(self, level: ba.Level) -> None:
"""Adds a ba.Level to the Campaign."""
if level.get_campaign() is not None:
raise Exception('level already belongs to a campaign')
if level.campaign is not None:
raise RuntimeError('Level already belongs to a campaign.')
level.set_campaign(self, len(self._levels))
self._levels.append(level)
def get_levels(self) -> List[ba.Level]:
"""Return the set of ba.Levels in the Campaign."""
@property
def levels(self) -> List[ba.Level]:
"""The list of ba.Levels in the Campaign."""
return self._levels
def get_level(self, name: str) -> ba.Level:
def getlevel(self, name: str) -> ba.Level:
"""Return a contained ba.Level by name."""
from ba import _error
for level in self._levels:
if level.name == name:
return level
raise Exception("Level '" + name + "' not found in campaign '" +
self.name + "'")
raise _error.NotFoundError("Level '" + name +
"' not found in campaign '" + self.name +
"'")
def reset(self) -> None:
"""Reset state for the Campaign."""
@ -87,14 +72,15 @@ class Campaign:
# FIXME should these give/take ba.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.get_config_dict()['Selection'] = levelname
self.configdict['Selection'] = levelname
_ba.app.config.commit()
def get_selected_level(self) -> str:
"""Return the name of the Level currently selected in the UI."""
return self.get_config_dict().get('Selection', self._levels[0].name)
return self.configdict.get('Selection', self._levels[0].name)
def get_config_dict(self) -> Dict[str, Any]:
@property
def configdict(self) -> Dict[str, Any]:
"""Return the live config dict for this campaign."""
val: Dict[str, Any] = (_ba.app.config.setdefault('Campaigns',
{}).setdefault(
@ -118,50 +104,53 @@ def init_campaigns() -> None:
from bastd.game.easteregghunt import EasterEggHuntGame
from bastd.game.ninjafight import NinjaFightGame
# TODO: Campaigns should be load-on-demand; not all imported at launch
# like this.
# FIXME: Once translations catch up, we can convert these to use the
# generic display-name '${GAME} Training' type stuff.
campaign = Campaign('Easy')
campaign.add_level(
campaign.addlevel(
_level.Level('Onslaught Training',
gametype=OnslaughtGame,
settings={'preset': 'training_easy'},
preview_texture_name='doomShroomPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Rookie Onslaught',
gametype=OnslaughtGame,
settings={'preset': 'rookie_easy'},
preview_texture_name='courtyardPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Rookie Football',
gametype=FootballCoopGame,
settings={'preset': 'rookie_easy'},
preview_texture_name='footballStadiumPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Pro Onslaught',
gametype=OnslaughtGame,
settings={'preset': 'pro_easy'},
preview_texture_name='doomShroomPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Pro Football',
gametype=FootballCoopGame,
settings={'preset': 'pro_easy'},
preview_texture_name='footballStadiumPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Pro Runaround',
gametype=RunaroundGame,
settings={'preset': 'pro_easy'},
preview_texture_name='towerDPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Uber Onslaught',
gametype=OnslaughtGame,
settings={'preset': 'uber_easy'},
preview_texture_name='courtyardPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Uber Football',
gametype=FootballCoopGame,
settings={'preset': 'uber_easy'},
preview_texture_name='footballStadiumPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Uber Runaround',
gametype=RunaroundGame,
settings={'preset': 'uber_easy'},
@ -170,52 +159,52 @@ def init_campaigns() -> None:
# "hard" mode
campaign = Campaign('Default')
campaign.add_level(
campaign.addlevel(
_level.Level('Onslaught Training',
gametype=OnslaughtGame,
settings={'preset': 'training'},
preview_texture_name='doomShroomPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Rookie Onslaught',
gametype=OnslaughtGame,
settings={'preset': 'rookie'},
preview_texture_name='courtyardPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Rookie Football',
gametype=FootballCoopGame,
settings={'preset': 'rookie'},
preview_texture_name='footballStadiumPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Pro Onslaught',
gametype=OnslaughtGame,
settings={'preset': 'pro'},
preview_texture_name='doomShroomPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Pro Football',
gametype=FootballCoopGame,
settings={'preset': 'pro'},
preview_texture_name='footballStadiumPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Pro Runaround',
gametype=RunaroundGame,
settings={'preset': 'pro'},
preview_texture_name='towerDPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Uber Onslaught',
gametype=OnslaughtGame,
settings={'preset': 'uber'},
preview_texture_name='courtyardPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Uber Football',
gametype=FootballCoopGame,
settings={'preset': 'uber'},
preview_texture_name='footballStadiumPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Uber Runaround',
gametype=RunaroundGame,
settings={'preset': 'uber'},
preview_texture_name='towerDPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('The Last Stand',
gametype=TheLastStandGame,
settings={},
@ -224,17 +213,17 @@ def init_campaigns() -> None:
# challenges: our 'official' random extra co-op levels
campaign = Campaign('Challenges', sequential=False)
campaign.add_level(
campaign.addlevel(
_level.Level('Infinite Onslaught',
gametype=OnslaughtGame,
settings={'preset': 'endless'},
preview_texture_name='doomShroomPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Infinite Runaround',
gametype=RunaroundGame,
settings={'preset': 'endless'},
preview_texture_name='towerDPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Race',
displayname='${GAME}',
gametype=RaceGame,
@ -244,7 +233,7 @@ def init_campaigns() -> None:
'Bomb Spawning': 0
},
preview_texture_name='bigGPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Pro Race',
displayname='Pro ${GAME}',
gametype=RaceGame,
@ -254,7 +243,7 @@ def init_campaigns() -> None:
'Bomb Spawning': 1000
},
preview_texture_name='bigGPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Lake Frigid Race',
displayname='${GAME}',
gametype=RaceGame,
@ -265,55 +254,55 @@ def init_campaigns() -> None:
'Bomb Spawning': 0
},
preview_texture_name='lakeFrigidPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Football',
displayname='${GAME}',
gametype=FootballCoopGame,
settings={'preset': 'tournament'},
preview_texture_name='footballStadiumPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Pro Football',
displayname='Pro ${GAME}',
gametype=FootballCoopGame,
settings={'preset': 'tournament_pro'},
preview_texture_name='footballStadiumPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Runaround',
displayname='${GAME}',
gametype=RunaroundGame,
settings={'preset': 'tournament'},
preview_texture_name='towerDPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Uber Runaround',
displayname='Uber ${GAME}',
gametype=RunaroundGame,
settings={'preset': 'tournament_uber'},
preview_texture_name='towerDPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('The Last Stand',
displayname='${GAME}',
gametype=TheLastStandGame,
settings={'preset': 'tournament'},
preview_texture_name='rampagePreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Tournament Infinite Onslaught',
displayname='Infinite Onslaught',
gametype=OnslaughtGame,
settings={'preset': 'endless_tournament'},
preview_texture_name='doomShroomPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Tournament Infinite Runaround',
displayname='Infinite Runaround',
gametype=RunaroundGame,
settings={'preset': 'endless_tournament'},
preview_texture_name='towerDPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Target Practice',
displayname='Pro ${GAME}',
gametype=TargetPracticeGame,
settings={},
preview_texture_name='doomShroomPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Target Practice B',
displayname='${GAME}',
gametype=TargetPracticeGame,
@ -323,38 +312,38 @@ def init_campaigns() -> None:
'Enable Triple Bombs': False
},
preview_texture_name='doomShroomPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Meteor Shower',
displayname='${GAME}',
gametype=MeteorShowerGame,
settings={},
preview_texture_name='rampagePreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Epic Meteor Shower',
displayname='${GAME}',
gametype=MeteorShowerGame,
settings={'Epic Mode': True},
preview_texture_name='rampagePreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Easter Egg Hunt',
displayname='${GAME}',
gametype=EasterEggHuntGame,
settings={},
preview_texture_name='towerDPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level('Pro Easter Egg Hunt',
displayname='Pro ${GAME}',
gametype=EasterEggHuntGame,
settings={'Pro Mode': True},
preview_texture_name='towerDPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level(
name='Ninja Fight', # (unique id not seen by player)
displayname='${GAME}', # (readable name seen by player)
gametype=NinjaFightGame,
settings={'preset': 'regular'},
preview_texture_name='courtyardPreview'))
campaign.add_level(
campaign.addlevel(
_level.Level(name='Pro Ninja Fight',
displayname='Pro ${GAME}',
gametype=NinjaFightGame,

View File

@ -0,0 +1,72 @@
# Released under the MIT License. See LICENSE for details.
#
"""Collision related functionality."""
from __future__ import annotations
from typing import TYPE_CHECKING
import _ba
from ba._error import NodeNotFoundError
if TYPE_CHECKING:
import ba
class Collision:
"""A class providing info about occurring collisions.
Category: Gameplay Classes
"""
@property
def position(self) -> ba.Vec3:
"""The position of the current collision."""
return _ba.Vec3(_ba.get_collision_info('position'))
@property
def sourcenode(self) -> ba.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).
"""
node = _ba.get_collision_info('sourcenode')
assert isinstance(node, (_ba.Node, type(None)))
if not node:
raise NodeNotFoundError()
return node
@property
def opposingnode(self) -> ba.Node:
"""The node the current callback material node is hitting.
Throws a ba.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)))
if not node:
raise NodeNotFoundError()
return node
@property
def opposingbody(self) -> int:
"""The body index on the opposing node in the current collision."""
body = _ba.get_collision_info('opposingbody')
assert isinstance(body, int)
return body
# Simply recycle one instance...
_collision = Collision()
def getcollision() -> Collision:
"""Return the in-progress collision.
Category: Gameplay Functions
"""
return _collision

View File

@ -1,49 +1,38 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality related to co-op games."""
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, TypeVar
import _ba
from ba._gameactivity import GameActivity
from ba._general import WeakCall
if TYPE_CHECKING:
from typing import Type, Dict, Any, Set, List, Sequence, Optional
from bastd.actor.playerspaz import PlayerSpaz
import ba
PlayerType = TypeVar('PlayerType', bound='ba.Player')
TeamType = TypeVar('TeamType', bound='ba.Team')
class CoopGameActivity(GameActivity):
class CoopGameActivity(GameActivity[PlayerType, TeamType]):
"""Base class for cooperative-mode games.
Category: Gameplay Classes
"""
# We can assume our session is a CoopSession.
session: ba.CoopSession
@classmethod
def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool:
from ba import _coopsession
return issubclass(sessiontype, _coopsession.CoopSession)
from ba._coopsession import CoopSession
return issubclass(sessiontype, CoopSession)
def __init__(self, settings: Dict[str, Any]):
def __init__(self, settings: dict):
super().__init__(settings)
# Cache these for efficiency.
@ -54,32 +43,31 @@ class CoopGameActivity(GameActivity):
self._warn_beeps_sound = _ba.getsound('warnBeeps')
def on_begin(self) -> None:
from ba import _general
super().on_begin()
# Show achievements remaining.
if not _ba.app.kiosk_mode:
_ba.timer(3.8,
_general.WeakCall(self._show_remaining_achievements))
if not (_ba.app.demo_mode or _ba.app.arcade_mode):
_ba.timer(3.8, WeakCall(self._show_remaining_achievements))
# Preload achievement images in case we get some.
_ba.timer(2.0, _general.WeakCall(self._preload_achievements))
_ba.timer(2.0, WeakCall(self._preload_achievements))
# Let's ask the server for a 'time-to-beat' value.
levelname = self._get_coop_level_name()
campaign = self.session.campaign
assert campaign is not None
config_str = (str(len(self.players)) + 'p' + campaign.get_level(
self.settings['name']).get_score_version_string().replace(
config_str = (str(len(self.players)) + 'p' + campaign.getlevel(
self.settings_raw['name']).get_score_version_string().replace(
' ', '_'))
_ba.get_scores_to_beat(levelname, config_str,
_general.WeakCall(self._on_got_scores_to_beat))
WeakCall(self._on_got_scores_to_beat))
def _on_got_scores_to_beat(self, scores: List[Dict[str, Any]]) -> None:
pass
def _show_standard_scores_to_beat_ui(self,
scores: List[Dict[str, Any]]) -> None:
from efro.util import asserttype
from ba._gameutils import timestring, animate
from ba._nodeactor import NodeActor
from ba._enums import TimeFormat
@ -87,7 +75,7 @@ class CoopGameActivity(GameActivity):
if scores is not None:
# Sort by originating date so that the most recent is first.
scores.sort(reverse=True, key=lambda s: s['time'])
scores.sort(reverse=True, key=lambda s: asserttype(s['time'], int))
# Now make a display for the most recent challenge.
for score in scores:
@ -116,7 +104,7 @@ class CoopGameActivity(GameActivity):
animate(txt.node, 'scale', {1.0: 0.0, 1.1: 0.7, 1.2: 0.6})
break
# FIXME: this is now redundant with activityutils.get_score_info();
# FIXME: this is now redundant with activityutils.getscoreconfig();
# need to kill this.
def get_score_type(self) -> str:
"""
@ -126,7 +114,8 @@ class CoopGameActivity(GameActivity):
def _get_coop_level_name(self) -> str:
assert self.session.campaign is not None
return self.session.campaign.name + ':' + str(self.settings['name'])
return self.session.campaign.name + ':' + str(
self.settings_raw['name'])
def celebrate(self, duration: float) -> None:
"""Tells all existing player-controlled characters to celebrate.
@ -141,26 +130,24 @@ class CoopGameActivity(GameActivity):
player.actor.handlemessage(CelebrateMessage(duration))
def _preload_achievements(self) -> None:
from ba import _achievement
achievements = _achievement.get_achievements_for_coop_level(
achievements = _ba.app.ach.achievements_for_coop_level(
self._get_coop_level_name())
for ach in achievements:
ach.get_icon_texture(True)
def _show_remaining_achievements(self) -> None:
# pylint: disable=cyclic-import
from ba import _achievement
from ba import _lang
from ba._language import Lstr
from bastd.actor.text import Text
ts_h_offs = 30
v_offs = -200
achievements = [
a for a in _achievement.get_achievements_for_coop_level(
a for a in _ba.app.ach.achievements_for_coop_level(
self._get_coop_level_name()) if not a.complete
]
vrmode = _ba.app.vr_mode
if achievements:
Text(_lang.Lstr(resource='achievementsRemainingText'),
Text(Lstr(resource='achievementsRemainingText'),
host_only=True,
position=(ts_h_offs - 10 + 40, v_offs - 10),
transition=Text.Transition.FADE_IN,
@ -186,7 +173,7 @@ class CoopGameActivity(GameActivity):
vval -= 55
def spawn_player_spaz(self,
player: ba.Player,
player: PlayerType,
position: Sequence[float] = (0.0, 0.0, 0.0),
angle: float = None) -> PlayerSpaz:
"""Spawn and wire up a standard player spaz."""
@ -204,12 +191,11 @@ class CoopGameActivity(GameActivity):
Returns True if a banner will be shown;
False otherwise
"""
from ba import _achievement
if achievement_name in self._achievements_awarded:
return
ach = _achievement.get_achievement(achievement_name)
ach = _ba.app.ach.get_achievement(achievement_name)
# If we're in the easy campaign and this achievement is hard-mode-only,
# ignore it.
@ -219,8 +205,8 @@ class CoopGameActivity(GameActivity):
if ach.hard_mode_only and campaign.name == 'Easy':
return
except Exception:
from ba import _error
_error.print_exception()
from ba._error import print_exception
print_exception()
# If we haven't awarded this one, check to see if we've got it.
# If not, set it through the game service *and* add a transaction
@ -243,7 +229,7 @@ class CoopGameActivity(GameActivity):
def fade_to_red(self) -> None:
"""Fade the screen to red; (such as when the good guys have lost)."""
from ba import _gameutils
c_existing = _gameutils.sharedobj('globals').tint
c_existing = self.globalsnode.tint
cnode = _ba.newnode('combine',
attrs={
'input0': c_existing[0],
@ -253,14 +239,13 @@ class CoopGameActivity(GameActivity):
})
_gameutils.animate(cnode, 'input1', {0: c_existing[1], 2.0: 0})
_gameutils.animate(cnode, 'input2', {0: c_existing[2], 2.0: 0})
cnode.connectattr('output', _gameutils.sharedobj('globals'), 'tint')
cnode.connectattr('output', self.globalsnode, 'tint')
def setup_low_life_warning_sound(self) -> None:
"""Set up a beeping noise to play when any players are near death."""
from ba import _general
self._life_warning_beep = None
self._life_warning_beep_timer = _ba.Timer(
1.0, _general.WeakCall(self._update_life_warning), repeat=True)
1.0, WeakCall(self._update_life_warning), repeat=True)
def _update_life_warning(self) -> None:
# Beep continuously if anyone is close to death.

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality related to coop-mode sessions."""
from __future__ import annotations
@ -30,8 +12,8 @@ if TYPE_CHECKING:
from typing import Any, List, Dict, Optional, Callable, Sequence
import ba
TEAM_COLORS = ((0.2, 0.4, 1.6), )
TEAM_NAMES = ('Good Guys', )
TEAM_COLORS = [(0.2, 0.4, 1.6)]
TEAM_NAMES = ['Good Guys']
class CoopSession(Session):
@ -42,16 +24,28 @@ class CoopSession(Session):
These generally consist of 1-4 players against
the computer and include functionality such as
high score lists.
Attributes:
campaign
The ba.Campaign instance this Session represents, or None if
there is no associated Campaign.
"""
use_teams = True
use_team_colors = False
allow_mid_activity_joins = False
# Note: even though these are instance vars, we annotate them at the
# class level so that docs generation can access their types.
campaign: Optional[ba.Campaign]
def __init__(self) -> None:
"""Instantiate a co-op mode session."""
# pylint: disable=cyclic-import
from ba._campaign import get_campaign
from ba._campaign import getcampaign
from bastd.activity.coopjoin import CoopJoinActivity
_ba.increment_analytics_count('Co-op session start')
app = _ba.app
# If they passed in explicit min/max, honor that.
@ -63,14 +57,7 @@ class CoopSession(Session):
if 'max_players' in app.coop_session_args:
max_players = app.coop_session_args['max_players']
else:
try:
max_players = app.config['Coop Game Max Players']
except Exception:
# Old pref value.
try:
max_players = app.config['Challenge Game Max Players']
except Exception:
max_players = 4
max_players = app.config.get('Coop Game Max Players', 4)
# print('FIXME: COOP SESSION WOULD CALC DEPS.')
depsets: Sequence[ba.DependencySet] = []
@ -78,32 +65,25 @@ class CoopSession(Session):
super().__init__(depsets,
team_names=TEAM_NAMES,
team_colors=TEAM_COLORS,
use_team_colors=False,
min_players=min_players,
max_players=max_players,
allow_mid_activity_joins=False)
max_players=max_players)
# Tournament-ID if we correspond to a co-op tournament (otherwise None)
self.tournament_id = (app.coop_session_args['tournament_id']
if 'tournament_id' in app.coop_session_args else
None)
self.tournament_id: Optional[str] = (
app.coop_session_args.get('tournament_id'))
# FIXME: Could be nice to pass this in as actual args.
self.campaign_state = {
'campaign': (app.coop_session_args['campaign']),
'level': app.coop_session_args['level']
}
self.campaign = get_campaign(self.campaign_state['campaign'])
self.campaign = getcampaign(app.coop_session_args['campaign'])
self.campaign_level_name: str = app.coop_session_args['level']
self._ran_tutorial_activity = False
self._tutorial_activity: Optional[ba.Activity] = None
self._custom_menu_ui: List[Dict[str, Any]] = []
# Start our joining screen.
self.set_activity(_ba.new_activity(CoopJoinActivity))
self.setactivity(_ba.newactivity(CoopJoinActivity))
self._next_game_instance: Optional[ba.GameActivity] = None
self._next_game_name: Optional[str] = None
self._next_game_level_name: Optional[str] = None
self._update_on_deck_game_instances()
def get_current_game_instance(self) -> ba.GameActivity:
@ -114,28 +94,27 @@ class CoopSession(Session):
# pylint: disable=cyclic-import
from ba._gameactivity import GameActivity
# Instantiates levels we might be running soon
# so they have time to load.
# Instantiate levels we may be running soon to let them load in the bg.
# Build an instance for the current level.
assert self.campaign is not None
level = self.campaign.get_level(self.campaign_state['level'])
level = self.campaign.getlevel(self.campaign_level_name)
gametype = level.gametype
settings = level.get_settings()
# Make sure all settings the game expects are present.
neededsettings = gametype.get_settings(type(self))
for settingname, setting in neededsettings:
if settingname not in settings:
settings[settingname] = setting['default']
neededsettings = gametype.get_available_settings(type(self))
for setting in neededsettings:
if setting.name not in settings:
settings[setting.name] = setting.default
newactivity = _ba.new_activity(gametype, settings)
newactivity = _ba.newactivity(gametype, settings)
assert isinstance(newactivity, GameActivity)
self._current_game_instance: GameActivity = newactivity
# Find the next level and build an instance for it too.
levels = self.campaign.get_levels()
level = self.campaign.get_level(self.campaign_state['level'])
levels = self.campaign.levels
level = self.campaign.getlevel(self.campaign_level_name)
nextlevel: Optional[ba.Level]
if level.index < len(levels) - 1:
@ -147,35 +126,35 @@ class CoopSession(Session):
settings = nextlevel.get_settings()
# Make sure all settings the game expects are present.
neededsettings = gametype.get_settings(type(self))
for settingname, setting in neededsettings:
if settingname not in settings:
settings[settingname] = setting['default']
neededsettings = gametype.get_available_settings(type(self))
for setting in neededsettings:
if setting.name not in settings:
settings[setting.name] = setting.default
# We wanna be in the activity's context while taking it down.
newactivity = _ba.new_activity(gametype, settings)
newactivity = _ba.newactivity(gametype, settings)
assert isinstance(newactivity, GameActivity)
self._next_game_instance = newactivity
self._next_game_name = nextlevel.name
self._next_game_level_name = nextlevel.name
else:
self._next_game_instance = None
self._next_game_name = None
self._next_game_level_name = None
# Special case:
# If our current level is 'onslaught training', instantiate
# our tutorial so its ready to go. (if we haven't run it yet).
if (self.campaign_state['level'] == 'Onslaught Training'
if (self.campaign_level_name == 'Onslaught Training'
and self._tutorial_activity is None
and not self._ran_tutorial_activity):
from bastd.tutorial import TutorialActivity
self._tutorial_activity = _ba.new_activity(TutorialActivity)
self._tutorial_activity = _ba.newactivity(TutorialActivity)
def get_custom_menu_entries(self) -> List[Dict[str, Any]]:
return self._custom_menu_ui
def on_player_leave(self, player: ba.Player) -> None:
def on_player_leave(self, sessionplayer: ba.SessionPlayer) -> None:
from ba._general import WeakCall
super().on_player_leave(player)
super().on_player_leave(sessionplayer)
# If all our players leave we wanna quit out of the session.
_ba.timer(2.0, WeakCall(self._end_session_if_empty))
@ -192,7 +171,7 @@ class CoopSession(Session):
# If there's *no* players left in the current activity but there *is*
# in the session, restart the activity to pull them into the game
# (or quit if they're just in the lobby).
if not activity.players and self.players:
if not activity.players and self.sessionplayers:
# Special exception for tourney games; don't auto-restart these.
if self.tournament_id is not None:
@ -213,7 +192,7 @@ class CoopSession(Session):
from bastd.ui.tournamententry import TournamentEntryWindow
from ba._gameactivity import GameActivity
activity = self.getactivity()
if activity is not None and not activity.is_expired():
if activity is not None and not activity.expired:
assert self.tournament_id is not None
assert isinstance(activity, GameActivity)
TournamentEntryWindow(tournament_id=self.tournament_id,
@ -229,13 +208,13 @@ class CoopSession(Session):
# Make an exception if there's no players left. Otherwise this
# can override the default session end that occurs in that case.
if not self.players:
if not self.sessionplayers:
return
# This method may get called from the UI context so make sure we
# explicitly run in the activity's context.
activity = self.getactivity()
if activity is not None and not activity.is_expired():
if activity is not None and not activity.expired:
activity.can_show_ad_on_death = True
with _ba.Context(activity):
activity.end(results={'outcome': 'restart'}, force=True)
@ -250,29 +229,28 @@ class CoopSession(Session):
# pylint: disable=too-many-statements
# pylint: disable=cyclic-import
from ba._activitytypes import JoinActivity, TransitionActivity
from ba._lang import Lstr
from ba._language import Lstr
from ba._general import WeakCall
from ba._coopgame import CoopGameActivity
from ba._gameresults import TeamGameResults
from ba._gameresults import GameResults
from ba._score import ScoreType
from ba._player import PlayerInfo
from bastd.tutorial import TutorialActivity
from bastd.activity.coopscore import CoopScoreScreen
app = _ba.app
# If we're running a TeamGameActivity we'll have a TeamGameResults
# If we're running a TeamGameActivity we'll have a GameResults
# as results. Otherwise its an old CoopGameActivity so its giving
# us a dict of random stuff.
if isinstance(results, TeamGameResults):
if isinstance(results, GameResults):
outcome = 'defeat' # This can't be 'beaten'.
else:
try:
outcome = results['outcome']
except Exception:
outcome = ''
outcome = '' if results is None else results.get('outcome', '')
# If 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).
active_players = [p for p in self.players if p.in_game]
active_players = [p for p in self.sessionplayers if p.in_game]
if not active_players:
self.end()
return
@ -284,9 +262,9 @@ class CoopSession(Session):
if outcome == 'next_level':
if self._next_game_instance is None:
raise Exception()
assert self._next_game_name is not None
self.campaign_state['level'] = self._next_game_name
raise RuntimeError()
assert self._next_game_level_name is not None
self.campaign_level_name = self._next_game_level_name
next_game = self._next_game_instance
else:
next_game = self._current_game_instance
@ -295,11 +273,11 @@ class CoopSession(Session):
# and will be going into onslaught-training, show the
# tutorial first.
if (isinstance(activity, JoinActivity)
and self.campaign_state['level'] == 'Onslaught Training'
and not app.kiosk_mode):
and self.campaign_level_name == 'Onslaught Training'
and not (app.demo_mode or app.arcade_mode)):
if self._tutorial_activity is None:
raise Exception('tutorial not preloaded properly')
self.set_activity(self._tutorial_activity)
raise RuntimeError('Tutorial not preloaded properly.')
self.setactivity(self._tutorial_activity)
self._tutorial_activity = None
self._ran_tutorial_activity = True
self._custom_menu_ui = []
@ -309,17 +287,17 @@ class CoopSession(Session):
# Reset stats for the new activity.
self.stats.reset()
for player in self.players:
for player in self.sessionplayers:
# Skip players that are still choosing a team.
if player.in_game:
self.stats.register_player(player)
self.stats.set_activity(next_game)
self.stats.register_sessionplayer(player)
self.stats.setactivity(next_game)
# Now flip the current activity.
self.set_activity(next_game)
# Now flip the current activity..
self.setactivity(next_game)
if not app.kiosk_mode:
if not (app.demo_mode or app.arcade_mode):
if self.tournament_id is not None:
self._custom_menu_ui = [{
'label':
@ -339,34 +317,38 @@ class CoopSession(Session):
# If we were in a tutorial, just pop a transition to get to the
# actual round.
elif isinstance(activity, TutorialActivity):
self.set_activity(_ba.new_activity(TransitionActivity))
self.setactivity(_ba.newactivity(TransitionActivity))
else:
# Generic team games.
if isinstance(results, TeamGameResults):
player_info = results.get_player_info()
score = results.get_team_score(results.get_teams()[0])
fail_message = None
score_order = ('decreasing' if results.get_lower_is_better()
else 'increasing')
if results.get_score_type() in ('seconds', 'milliseconds',
'time'):
score_type = 'time'
playerinfos: List[ba.PlayerInfo]
# Results contains milliseconds; ScoreScreen wants
# hundredths; need to fix :-/
# Generic team games.
if isinstance(results, GameResults):
playerinfos = results.playerinfos
score = results.get_sessionteam_score(results.sessionteams[0])
fail_message = None
score_order = ('decreasing'
if results.lower_is_better else 'increasing')
if results.scoretype in (ScoreType.SECONDS,
ScoreType.MILLISECONDS):
scoretype = 'time'
# ScoreScreen wants hundredths of a second.
if score is not None:
score //= 10
if results.scoretype is ScoreType.SECONDS:
score *= 100
elif results.scoretype is ScoreType.MILLISECONDS:
score //= 10
else:
raise RuntimeError('FIXME')
else:
if results.get_score_type() != 'points':
print(("Unknown score type: '" +
results.get_score_type() + "'"))
score_type = 'points'
if results.scoretype is not ScoreType.POINTS:
print(f'Unknown ScoreType:' f' "{results.scoretype}"')
scoretype = 'points'
# Old coop-game-specific results; should migrate away from these.
else:
player_info = (results['player_info']
if 'player_info' in results else None)
playerinfos = results.get('playerinfos')
score = results['score'] if 'score' in results else None
fail_message = (results['fail_message']
if 'fail_message' in results else None)
@ -375,26 +357,31 @@ class CoopSession(Session):
activity_score_type = (activity.get_score_type() if isinstance(
activity, CoopGameActivity) else None)
assert activity_score_type is not None
score_type = activity_score_type
scoretype = activity_score_type
# Validate types.
if playerinfos is not None:
assert isinstance(playerinfos, list)
assert (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.set_activity(_ba.new_activity(TransitionActivity))
self.setactivity(_ba.newactivity(TransitionActivity))
else:
self.set_activity(
_ba.new_activity(
self.setactivity(
_ba.newactivity(
CoopScoreScreen, {
'player_info': player_info,
'playerinfos': playerinfos,
'score': score,
'fail_message': fail_message,
'score_order': score_order,
'score_type': score_type,
'score_type': scoretype,
'outcome': outcome,
'campaign': self.campaign,
'level': self.campaign_state['level']
'level': self.campaign_level_name
}))
# No matter what, get the next 2 levels ready to go.

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality related to object/asset dependencies."""
from __future__ import annotations
@ -283,7 +265,7 @@ class DependencySet(Generic[T]):
# Watch for wacky infinite dep loops.
if recursion > 10:
raise Exception('Max recursion reached')
raise RecursionError('Max recursion reached')
hashval = dep.get_hash()
@ -422,8 +404,8 @@ def test_depset() -> None:
if dep.cls is AssetPackage:
print('MISSING ASSET PACKAGE', dep.config)
else:
raise Exception('unknown dependency error for ' +
str(dep.cls))
raise RuntimeError(
f'Unknown dependency error for {dep.cls}') from exc
except Exception as exc:
print('DependencySet resolve failed with exc type:', type(exc))
if depset.resolved:

View File

@ -1,41 +1,27 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality related to teams sessions."""
from __future__ import annotations
from typing import TYPE_CHECKING
import _ba
from ba import _multiteamsession
from ba._multiteamsession import MultiTeamSession
if TYPE_CHECKING:
import ba
class DualTeamSession(_multiteamsession.MultiTeamSession):
class DualTeamSession(MultiTeamSession):
"""ba.Session type for teams mode games.
Category: Gameplay Classes
"""
_use_teams = True
# Base class overrides:
use_teams = True
use_team_colors = True
_playlist_selection_var = 'Team Tournament Playlist Selection'
_playlist_randomize_var = 'Team Tournament Playlist Randomize'
_playlists_var = 'Team Tournament Playlists'
@ -44,29 +30,28 @@ class DualTeamSession(_multiteamsession.MultiTeamSession):
_ba.increment_analytics_count('Teams session start')
super().__init__()
def _switch_to_score_screen(self, results: ba.TeamGameResults) -> None:
def _switch_to_score_screen(self, results: ba.GameResults) -> None:
# pylint: disable=cyclic-import
from bastd.activity.drawscore import DrawScoreScreenActivity
from bastd.activity.dualteamscore import (
TeamVictoryScoreScreenActivity)
from bastd.activity.multiteamvictory import (
TeamSeriesVictoryScoreScreenActivity)
winners = results.get_winners()
winnergroups = results.winnergroups
# If everyone has the same score, call it a draw.
if len(winners) < 2:
self.set_activity(_ba.new_activity(DrawScoreScreenActivity))
if len(winnergroups) < 2:
self.setactivity(_ba.newactivity(DrawScoreScreenActivity))
else:
winner = winners[0].teams[0]
winner.sessiondata['score'] += 1
winner = winnergroups[0].teams[0]
winner.customdata['score'] += 1
# If a team has won, show final victory screen.
if winner.sessiondata['score'] >= (self._series_length -
1) / 2 + 1:
self.set_activity(
_ba.new_activity(TeamSeriesVictoryScoreScreenActivity,
{'winner': winner}))
if winner.customdata['score'] >= (self._series_length - 1) / 2 + 1:
self.setactivity(
_ba.newactivity(TeamSeriesVictoryScoreScreenActivity,
{'winner': winner}))
else:
self.set_activity(
_ba.new_activity(TeamVictoryScoreScreenActivity,
{'winner': winner}))
self.setactivity(
_ba.newactivity(TeamVictoryScoreScreenActivity,
{'winner': winner}))

View File

@ -1,28 +1,67 @@
# Copyright (c) 2011-2020 Eric Froemling
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
# Released under the MIT License. See LICENSE for details.
"""Enums generated by tools/update_python_enums_module in ba-internal."""
from enum import Enum
class InputType(Enum):
"""Types of input a controller can send to the game.
Category: Enums
"""
UP_DOWN = 2
LEFT_RIGHT = 3
JUMP_PRESS = 4
JUMP_RELEASE = 5
PUNCH_PRESS = 6
PUNCH_RELEASE = 7
BOMB_PRESS = 8
BOMB_RELEASE = 9
PICK_UP_PRESS = 10
PICK_UP_RELEASE = 11
RUN = 12
FLY_PRESS = 13
FLY_RELEASE = 14
START_PRESS = 15
START_RELEASE = 16
HOLD_POSITION_PRESS = 17
HOLD_POSITION_RELEASE = 18
LEFT_PRESS = 19
LEFT_RELEASE = 20
RIGHT_PRESS = 21
RIGHT_RELEASE = 22
UP_PRESS = 23
UP_RELEASE = 24
DOWN_PRESS = 25
DOWN_RELEASE = 26
class UIScale(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.
"""
LARGE = 0
MEDIUM = 1
SMALL = 2
class TimeType(Enum):
"""Specifies the type of time for various operations to target/use.

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Error related functionality."""
from __future__ import annotations
@ -31,16 +13,6 @@ if TYPE_CHECKING:
import ba
class _UnhandledType:
pass
# A special value that should be returned from handlemessage()
# functions for unhandled message types. This may result
# in fallback message types being attempted/etc.
UNHANDLED = _UnhandledType()
class DependencyError(Exception):
"""Exception raised when one or more ba.Dependency items are missing.
@ -59,6 +31,16 @@ class DependencyError(Exception):
return self._deps
class ContextError(Exception):
"""Exception raised when a call is made in an invalid context.
category: Exception Classes
Examples of this include calling UI functions within an Activity context
or calling scene manipulation functions outside of a game context.
"""
class NotFoundError(Exception):
"""Exception raised when a referenced object does not exist.
@ -73,6 +55,13 @@ class PlayerNotFoundError(NotFoundError):
"""
class SessionPlayerNotFoundError(NotFoundError):
"""Exception raised when an expected ba.SessionPlayer does not exist.
category: Exception Classes
"""
class TeamNotFoundError(NotFoundError):
"""Exception raised when an expected ba.Team does not exist.
@ -80,6 +69,20 @@ class TeamNotFoundError(NotFoundError):
"""
class DelegateNotFoundError(NotFoundError):
"""Exception raised when an expected delegate object does not exist.
category: Exception Classes
"""
class SessionTeamNotFoundError(NotFoundError):
"""Exception raised when an expected ba.SessionTeam does not exist.
category: Exception Classes
"""
class NodeNotFoundError(NotFoundError):
"""Exception raised when an expected ba.Node does not exist.
@ -136,7 +139,7 @@ def print_exception(*args: Any, **keywds: Any) -> None:
if keywds:
allowed_keywds = ['once']
if any(keywd not in allowed_keywds for keywd in keywds):
raise Exception('invalid keyword(s)')
raise TypeError('invalid keyword(s)')
try:
# If we're only printing once and already have, bail.
if keywds.get('once', False):

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality related to free-for-all sessions."""
from __future__ import annotations
@ -37,7 +19,8 @@ class FreeForAllSession(MultiTeamSession):
Category: Gameplay Classes
"""
_use_teams = False
use_teams = False
use_team_colors = False
_playlist_selection_var = 'Free-for-All Playlist Selection'
_playlist_randomize_var = 'Free-for-All Playlist Randomize'
_playlists_var = 'Free-for-All Playlists'
@ -48,17 +31,17 @@ class FreeForAllSession(MultiTeamSession):
This is based on the current number of players.
"""
point_awards: Dict[int, int]
if len(self.players) == 1:
if len(self.sessionplayers) == 1:
point_awards = {}
elif len(self.players) == 2:
elif len(self.sessionplayers) == 2:
point_awards = {0: 6}
elif len(self.players) == 3:
elif len(self.sessionplayers) == 3:
point_awards = {0: 6, 1: 3}
elif len(self.players) == 4:
elif len(self.sessionplayers) == 4:
point_awards = {0: 8, 1: 4, 2: 2}
elif len(self.players) == 5:
elif len(self.sessionplayers) == 5:
point_awards = {0: 8, 1: 4, 2: 2}
elif len(self.players) == 6:
elif len(self.sessionplayers) == 6:
point_awards = {0: 8, 1: 4, 2: 2}
else:
point_awards = {0: 8, 1: 4, 2: 2, 3: 1}
@ -68,21 +51,21 @@ class FreeForAllSession(MultiTeamSession):
_ba.increment_analytics_count('Free-for-all session start')
super().__init__()
def _switch_to_score_screen(self, results: ba.TeamGameResults) -> None:
def _switch_to_score_screen(self, results: ba.GameResults) -> None:
# pylint: disable=cyclic-import
from efro.util import asserttype
from bastd.activity.drawscore import DrawScoreScreenActivity
from bastd.activity.multiteamvictory import (
TeamSeriesVictoryScoreScreenActivity)
from bastd.activity.freeforallvictory import (
FreeForAllVictoryScoreScreenActivity)
winners = results.get_winners()
winners = results.winnergroups
# If there's multiple players and everyone has the same score,
# call it a draw.
if len(self.players) > 1 and len(winners) < 2:
self.set_activity(
_ba.new_activity(DrawScoreScreenActivity,
{'results': results}))
if len(self.sessionplayers) > 1 and len(winners) < 2:
self.setactivity(
_ba.newactivity(DrawScoreScreenActivity, {'results': results}))
else:
# Award different point amounts based on number of players.
point_awards = self.get_ffa_point_awards()
@ -90,24 +73,25 @@ class FreeForAllSession(MultiTeamSession):
for i, winner in enumerate(winners):
for team in winner.teams:
points = (point_awards[i] if i in point_awards else 0)
team.sessiondata['previous_score'] = (
team.sessiondata['score'])
team.sessiondata['score'] += points
team.customdata['previous_score'] = (
team.customdata['score'])
team.customdata['score'] += points
series_winners = [
team for team in self.teams
if team.sessiondata['score'] >= self._ffa_series_length
team for team in self.sessionteams
if team.customdata['score'] >= self._ffa_series_length
]
series_winners.sort(reverse=True,
key=lambda tm: (tm.sessiondata['score']))
series_winners.sort(
reverse=True,
key=lambda t: asserttype(t.customdata['score'], int))
if (len(series_winners) == 1
or (len(series_winners) > 1
and series_winners[0].sessiondata['score'] !=
series_winners[1].sessiondata['score'])):
self.set_activity(
_ba.new_activity(TeamSeriesVictoryScoreScreenActivity,
{'winner': series_winners[0]}))
and series_winners[0].customdata['score'] !=
series_winners[1].customdata['score'])):
self.setactivity(
_ba.newactivity(TeamSeriesVictoryScoreScreenActivity,
{'winner': series_winners[0]}))
else:
self.set_activity(
_ba.new_activity(FreeForAllVictoryScoreScreenActivity,
{'results': results}))
self.setactivity(
_ba.newactivity(FreeForAllVictoryScoreScreenActivity,
{'results': results}))

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality related to game results."""
from __future__ import annotations
@ -26,9 +8,12 @@ import weakref
from dataclasses import dataclass
from typing import TYPE_CHECKING
from efro.util import asserttype
from ba._team import Team, SessionTeam
if TYPE_CHECKING:
from weakref import ReferenceType
from typing import Sequence, Tuple, Any, Optional, Dict, List
from typing import Sequence, Tuple, Any, Optional, Dict, List, Union
import ba
@ -36,12 +21,12 @@ if TYPE_CHECKING:
class WinnerGroup:
"""Entry for a winning team or teams calculated by game-results."""
score: Optional[int]
teams: Sequence[ba.Team]
teams: Sequence[ba.SessionTeam]
class TeamGameResults:
class GameResults:
"""
Results for a completed ba.TeamGameActivity.
Results for a completed game.
Category: Gameplay Classes
@ -50,135 +35,147 @@ class TeamGameResults:
"""
def __init__(self) -> None:
"""Instantiate a results instance."""
self._game_set = False
self._scores: Dict[int, Tuple[ReferenceType[ba.Team],
self._scores: Dict[int, Tuple[ReferenceType[ba.SessionTeam],
Optional[int]]] = {}
self._teams: Optional[List[ReferenceType[ba.Team]]] = None
self._player_info: Optional[List[Dict[str, Any]]] = None
self._sessionteams: Optional[List[ReferenceType[
ba.SessionTeam]]] = None
self._playerinfos: Optional[List[ba.PlayerInfo]] = None
self._lower_is_better: Optional[bool] = None
self._score_name: Optional[str] = None
self._score_label: Optional[str] = None
self._none_is_winner: Optional[bool] = None
self._score_type: Optional[str] = None
self._scoretype: Optional[ba.ScoreType] = None
def set_game(self, game: ba.GameActivity) -> None:
"""Set the game instance these results are applying to."""
if self._game_set:
raise RuntimeError('Game set twice for TeamGameResults.')
raise RuntimeError('Game set twice for GameResults.')
self._game_set = True
self._teams = [weakref.ref(team) for team in game.teams]
score_info = game.get_resolved_score_info()
self._player_info = copy.deepcopy(game.initial_player_info)
self._lower_is_better = score_info['lower_is_better']
self._score_name = score_info['score_name']
self._none_is_winner = score_info['none_is_winner']
self._score_type = score_info['score_type']
self._sessionteams = [
weakref.ref(team.sessionteam) for team in game.teams
]
scoreconfig = game.getscoreconfig()
self._playerinfos = copy.deepcopy(game.initialplayerinfos)
self._lower_is_better = scoreconfig.lower_is_better
self._score_label = scoreconfig.label
self._none_is_winner = scoreconfig.none_is_winner
self._scoretype = scoreconfig.scoretype
def set_team_score(self, team: ba.Team, score: int) -> None:
"""Set the score for a given ba.Team.
def set_team_score(self, team: ba.Team, score: Optional[int]) -> None:
"""Set the score for a given team.
This can be a number or None.
(see the none_is_winner arg in the constructor)
"""
self._scores[team.get_id()] = (weakref.ref(team), score)
assert isinstance(team, Team)
sessionteam = team.sessionteam
self._scores[sessionteam.id] = (weakref.ref(sessionteam), score)
def get_team_score(self, team: ba.Team) -> Optional[int]:
"""Return the score for a given team."""
def get_sessionteam_score(self,
sessionteam: ba.SessionTeam) -> Optional[int]:
"""Return the score for a given ba.SessionTeam."""
assert isinstance(sessionteam, SessionTeam)
for score in list(self._scores.values()):
if score[0]() is team:
if score[0]() is sessionteam:
return score[1]
# If we have no score value, assume None.
return None
def get_teams(self) -> List[ba.Team]:
"""Return all ba.Teams in the results."""
@property
def sessionteams(self) -> List[ba.SessionTeam]:
"""Return all ba.SessionTeams in the results."""
if not self._game_set:
raise RuntimeError("Can't get teams until game is set.")
teams = []
assert self._teams is not None
for team_ref in self._teams:
assert self._sessionteams is not None
for team_ref in self._sessionteams:
team = team_ref()
if team is not None:
teams.append(team)
return teams
def has_score_for_team(self, team: ba.Team) -> bool:
"""Return whether there is a score for a given team."""
for score in list(self._scores.values()):
if score[0]() is team:
return True
return False
def has_score_for_sessionteam(self, sessionteam: ba.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_team_score_str(self, team: ba.Team) -> ba.Lstr:
"""Return the score for the given ba.Team as an Lstr.
def get_sessionteam_score_str(self,
sessionteam: ba.SessionTeam) -> ba.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._lang import Lstr
from ba._language import Lstr
from ba._enums import TimeFormat
from ba._score import ScoreType
if not self._game_set:
raise RuntimeError("Can't get team-score-str until game is set.")
for score in list(self._scores.values()):
if score[0]() is team:
if score[0]() is sessionteam:
if score[1] is None:
return Lstr(value='-')
if self._score_type == 'seconds':
if self._scoretype is ScoreType.SECONDS:
return timestring(score[1] * 1000,
centi=False,
timeformat=TimeFormat.MILLISECONDS)
if self._score_type == 'milliseconds':
if self._scoretype is ScoreType.MILLISECONDS:
return timestring(score[1],
centi=True,
timeformat=TimeFormat.MILLISECONDS)
return Lstr(value=str(score[1]))
return Lstr(value='-')
def get_player_info(self) -> List[Dict[str, Any]]:
@property
def playerinfos(self) -> List[ba.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.")
assert self._player_info is not None
return self._player_info
assert self._playerinfos is not None
return self._playerinfos
def get_score_type(self) -> str:
"""Get the type of score."""
@property
def scoretype(self) -> ba.ScoreType:
"""The type of score."""
if not self._game_set:
raise RuntimeError("Can't get score-type until game is set.")
assert self._score_type is not None
return self._score_type
assert self._scoretype is not None
return self._scoretype
def get_score_name(self) -> str:
"""Get the name associated with scores ('points', etc)."""
@property
def score_label(self) -> str:
"""The label associated with scores ('points', etc)."""
if not self._game_set:
raise RuntimeError("Can't get score-name until game is set.")
assert self._score_name is not None
return self._score_name
raise RuntimeError("Can't get score-label until game is set.")
assert self._score_label is not None
return self._score_label
def get_lower_is_better(self) -> bool:
"""Return whether lower scores are better."""
@property
def lower_is_better(self) -> bool:
"""Whether lower scores are better."""
if not self._game_set:
raise RuntimeError("Can't get lower-is-better until game is set.")
assert self._lower_is_better is not None
return self._lower_is_better
def get_winning_team(self) -> Optional[ba.Team]:
"""Get the winning ba.Team if there is exactly one; None otherwise."""
@property
def winning_sessionteam(self) -> Optional[ba.SessionTeam]:
"""The winning ba.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.get_winners()
winners = self.winnergroups
if winners and len(winners[0].teams) == 1:
return winners[0].teams[0]
return None
def get_winners(self) -> List[WinnerGroup]:
@property
def winnergroups(self) -> List[WinnerGroup]:
"""Get an ordered list of winner groups."""
if not self._game_set:
raise RuntimeError("Can't get winners until game is set.")
# Group by best scoring teams.
winners: Dict[int, List[ba.Team]] = {}
winners: Dict[int, List[ba.SessionTeam]] = {}
scores = [
score for score in self._scores.values()
if score[0]() is not None and score[1] is not None
@ -190,21 +187,23 @@ class TeamGameResults:
assert team is not None
sval.append(team)
results: List[Tuple[Optional[int],
List[ba.Team]]] = list(winners.items())
results.sort(reverse=not self._lower_is_better, key=lambda x: x[0])
List[ba.SessionTeam]]] = list(winners.items())
results.sort(reverse=not self._lower_is_better,
key=lambda x: asserttype(x[0], int))
# Also group the 'None' scores.
none_teams: List[ba.Team] = []
none_sessionteams: List[ba.SessionTeam] = []
for score in self._scores.values():
scoreteam = score[0]()
if scoreteam is not None and score[1] is None:
none_teams.append(scoreteam)
none_sessionteams.append(scoreteam)
# Add the Nones to the list (either as winners or losers
# depending on the rules).
if none_teams:
nones: List[Tuple[Optional[int],
List[ba.Team]]] = [(None, none_teams)]
if none_sessionteams:
nones: List[Tuple[Optional[int], List[ba.SessionTeam]]] = [
(None, none_sessionteams)
]
if self._none_is_winner:
results = nones + results
else:

View File

@ -1,30 +1,15 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Utility functionality pertaining to gameplay."""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
import _ba
from ba._enums import TimeType, TimeFormat, SpecialChar
from ba._enums import TimeType, TimeFormat, SpecialChar, UIScale
from ba._error import ActivityNotFoundError
if TYPE_CHECKING:
from typing import Any, Dict, Sequence, Optional
@ -40,6 +25,17 @@ TROPHY_CHARS = {
}
@dataclass
class GameTip:
"""Defines a tip presentable to the user at the start of a game.
Category: Gameplay Classes
"""
text: str
icon: Optional[ba.Texture] = None
sound: Optional[ba.Sound] = 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:
@ -47,125 +43,6 @@ def get_trophy_string(trophy_id: str) -> str:
return '?'
def sharedobj(name: str) -> Any:
"""Return a predefined object for the current Activity, creating if needed.
Category: Gameplay Functions
Available values for 'name':
'globals': returns the 'globals' ba.Node, containing various global
controls & values.
'object_material': a ba.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)
'player_material': a ba.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.
'pickup_material': a ba.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.
'footing_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.
'attack_material': a ba.Material applied to explosion shapes, punch
shapes, etc. An object not wanting to receive impulse/etc messages can
disable collisions against this material.
'death_material': a ba.Material that sends a ba.DieMessage() to anything
that touches it; handy for terrain below a cliff, etc.
'region_material': a ba.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.
'railing_material': a ba.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.
"""
# pylint: disable=too-many-branches
from ba._messages import DieMessage
# We store these on the current context; whether its an activity or
# session.
activity: Optional[ba.Activity] = _ba.getactivity(doraise=False)
if activity is not None:
# Grab shared-objs dict.
sharedobjs = getattr(activity, 'sharedobjs', None)
# Grab item out of it.
try:
return sharedobjs[name]
except Exception:
pass
obj: Any
# Hmm looks like it doesn't yet exist; create it if its a valid value.
if name == 'globals':
node_obj = _ba.newnode('globals')
obj = node_obj
elif name in [
'object_material', 'player_material', 'pickup_material',
'footing_material', 'attack_material'
]:
obj = _ba.Material()
elif name == 'death_material':
mat = obj = _ba.Material()
mat.add_actions(
('message', 'their_node', 'at_connect', DieMessage()))
elif name == 'region_material':
obj = _ba.Material()
elif name == 'railing_material':
mat = obj = _ba.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))
mat.add_actions(conditions=('they_have_material',
sharedobj('player_material')),
actions=(('modify_part_collision', 'collide',
True), ('modify_part_collision',
'friction', 0.0)))
else:
raise Exception(
"unrecognized shared object (activity context): '" + name +
"'")
else:
session: Optional[ba.Session] = _ba.getsession(doraise=False)
if session is not None:
# Grab shared-objs dict (creating if necessary).
sharedobjs = session.sharedobjs
# Grab item out of it.
obj = sharedobjs.get(name)
if obj is not None:
return obj
# Hmm looks like it doesn't yet exist; create if its a valid value.
if name == 'globals':
obj = _ba.newnode('sessionglobals')
else:
raise Exception('unrecognized shared object '
"(session context): '" + name + "'")
else:
raise Exception('no current activity or session context')
# Ok, got a shiny new shared obj; store it for quick access next time.
sharedobjs[name] = obj
return obj
def animate(node: ba.Node,
attr: str,
keys: Dict[float, float],
@ -194,11 +71,10 @@ def animate(node: ba.Node,
# Temp sanity check while we transition from milliseconds to seconds
# based time values.
if _ba.app.test_build and not suppress_format_warning:
for item in items:
# (PyCharm seems to think item is a float, not a tuple)
# noinspection PyUnresolvedReferences
_ba.time_format_check(timeformat, item[0])
if __debug__:
if not suppress_format_warning:
for item in items:
_ba.time_format_check(timeformat, item[0])
curve = _ba.newnode('animcurve',
owner=node,
@ -209,7 +85,7 @@ def animate(node: ba.Node,
elif timeformat is TimeFormat.MILLISECONDS:
mult = 1
else:
raise Exception(f'invalid timeformat value: {timeformat}')
raise ValueError(f'invalid timeformat value: {timeformat}')
curve.times = [int(mult * time) for time, val in items]
curve.offset = _ba.time(timeformat=TimeFormat.MILLISECONDS) + int(
@ -222,15 +98,20 @@ def animate(node: ba.Node,
# FIXME: Even if we are looping we should have a way to die once we
# get disconnected.
if not loop:
# (PyCharm seems to think item is a float, not a tuple)
# noinspection PyUnresolvedReferences
_ba.timer(int(mult * items[-1][0]) + 1000,
curve.delete,
timeformat=TimeFormat.MILLISECONDS)
# Do the connects last so all our attrs are in place when we push initial
# values through.
sharedobj('globals').connectattr(driver, curve, 'in')
# We operate in either activities or sessions..
try:
globalsnode = _ba.getactivity().globalsnode
except ActivityNotFoundError:
globalsnode = _ba.getsession().sessionglobalsnode
globalsnode.connectattr(driver, curve, 'in')
curve.connectattr('out', node, attr)
return curve
@ -261,25 +142,31 @@ def animate_array(node: ba.Node,
# Temp sanity check while we transition from milliseconds to seconds
# based time values.
if _ba.app.test_build and not suppress_format_warning:
for item in items:
# (PyCharm seems to think item is a float, not a tuple)
# noinspection PyUnresolvedReferences
_ba.time_format_check(timeformat, item[0])
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 Exception('invalid timeformat value: "' + str(timeformat) + '"')
raise ValueError('invalid timeformat value: "' + str(timeformat) + '"')
# We operate in either activities or sessions..
try:
globalsnode = _ba.getactivity().globalsnode
except ActivityNotFoundError:
globalsnode = _ba.getsession().sessionglobalsnode
for i in range(size):
curve = _ba.newnode('animcurve',
owner=node,
name=('Driving ' + str(node) + ' \'' + attr +
'\' member ' + str(i)))
sharedobj('globals').connectattr(driver, curve, 'in')
globalsnode.connectattr(driver, 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(
@ -291,7 +178,6 @@ def animate_array(node: ba.Node,
# curve after its done its job.
if not loop:
# (PyCharm seems to think item is a float, not a tuple)
# noinspection PyUnresolvedReferences
_ba.timer(int(mult * items[-1][0]) + 1000,
curve.delete,
timeformat=TimeFormat.MILLISECONDS)
@ -303,7 +189,6 @@ def animate_array(node: ba.Node,
# once we get disconnected.
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)
@ -321,7 +206,7 @@ def show_damage_count(damage: str, position: Sequence[float],
# 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.interface_type == 'small' or app.vr_mode
do_big = app.ui.uiscale is UIScale.SMALL or app.vr_mode
txtnode = _ba.newnode('text',
attrs={
'text': damage,
@ -382,12 +267,13 @@ def timestring(timeval: float,
use a 'timedisplay' Node and attribute connections.
"""
from ba._lang import Lstr
from ba._language import Lstr
# Temp sanity check while we transition from milliseconds to seconds
# based time values.
if _ba.app.test_build and not suppress_format_warning:
_ba.time_format_check(timeformat, timeval)
if __debug__:
if not suppress_format_warning:
_ba.time_format_check(timeformat, timeval)
# We operate on milliseconds internally.
if timeformat is TimeFormat.SECONDS:
@ -395,7 +281,7 @@ def timestring(timeval: float,
elif timeformat is TimeFormat.MILLISECONDS:
pass
else:
raise Exception(f'invalid timeformat: {timeformat}')
raise ValueError(f'invalid timeformat: {timeformat}')
if not isinstance(timeval, int):
timeval = int(timeval)
bits = []
@ -452,7 +338,6 @@ 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()
# noinspection PyTypeHints
activity.camera_flash_data = [] # type: ignore
for i in range(6):
light = NodeActor(

View File

@ -1,39 +1,62 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Utility snippets applying to generic Python code."""
from __future__ import annotations
import gc
import types
import weakref
from typing import TYPE_CHECKING, TypeVar
import random
import inspect
from typing import TYPE_CHECKING, TypeVar, Protocol
from efro.terminal import Clr
import _ba
from ba._error import print_error, print_exception
from ba._enums import TimeType
if TYPE_CHECKING:
from typing import Any, Type
from efro.call import Call
from types import FrameType
from typing import Any, Type, Optional
from efro.call import Call as Call # 'as Call' so we re-export.
from weakref import ReferenceType
class Existable(Protocol):
"""A Protocol for objects supporting an exists() method.
Category: Protocols
"""
def exists(self) -> bool:
"""Whether this object exists."""
...
ExistableType = TypeVar('ExistableType', bound=Existable)
T = TypeVar('T')
def existing(obj: Optional[ExistableType]) -> Optional[ExistableType]:
"""Convert invalid references to None for any ba.Existable object.
Category: Gameplay Functions
To best support type checking, it is important that invalid references
not be passed around and instead get converted to values of None.
That way the type checker can properly flag attempts to pass dead
objects (Optional[FooType]) into functions expecting only live ones
(FooType), etc. This call can be used on any 'existable' object
(one with an exists() method) and will convert it to a None value
if it does not exist.
For more info, see notes on 'existables' here:
https://ballistica.net/wiki/Coding-Style-Guide
"""
assert obj is None or hasattr(obj, 'exists'), f'No "exists" on {obj}'
return obj if obj is not None and obj.exists() else None
def getclass(name: str, subclassof: Type[T]) -> Type[T]:
"""Given a full class name such as foo.bar.MyClass, return the class.
@ -50,7 +73,7 @@ def getclass(name: str, subclassof: Type[T]) -> Type[T]:
cls: Type = getattr(module, classname)
if not issubclass(cls, subclassof):
raise TypeError(name + ' is not a subclass of ' + str(subclassof))
raise TypeError(f'{name} is not a subclass of {subclassof}.')
return cls
@ -68,27 +91,23 @@ def json_prep(data: Any) -> Any:
if isinstance(data, list):
return [json_prep(element) for element in data]
if isinstance(data, tuple):
from ba import _error
_error.print_error('json_prep encountered tuple', once=True)
print_error('json_prep encountered tuple', once=True)
return [json_prep(element) for element in data]
if isinstance(data, bytes):
try:
return data.decode(errors='ignore')
except Exception:
from ba import _error
_error.print_error('json_prep encountered utf-8 decode error',
once=True)
print_error('json_prep encountered utf-8 decode error', once=True)
return data.decode(errors='ignore')
if not isinstance(data, (str, float, bool, type(None), int)):
from ba import _error
_error.print_error('got unsupported type in json_prep:' +
str(type(data)),
once=True)
print_error('got unsupported type in json_prep:' + str(type(data)),
once=True)
return data
def utf8_all(data: Any) -> Any:
"""Convert any unicode data in provided sequence(s)to utf8 bytes."""
"""Convert any unicode data in provided sequence(s) to utf8 bytes."""
if isinstance(data, dict):
return dict((utf8_all(key), utf8_all(value))
for key, value in list(data.items()))
@ -103,7 +122,6 @@ def utf8_all(data: Any) -> Any:
def print_refs(obj: Any) -> None:
"""Print a list of known live references to an object."""
import gc
# Hmmm; I just noticed that calling this on an object
# seems to keep it alive. Should figure out why.
@ -154,9 +172,10 @@ class _WeakCall:
"""
def __init__(self, *args: Any, **keywds: Any) -> None:
"""
Instantiate a WeakCall; pass a callable as the first
arg, followed by any number of arguments or keywords.
"""Instantiate a WeakCall.
Pass a callable as the first arg, followed by any number of
arguments or keywords.
# Example: wrap a method call with some positional and
# keyword args:
@ -206,9 +225,10 @@ class _Call:
"""
def __init__(self, *args: Any, **keywds: Any):
"""
Instantiate a Call; pass a callable as the first
arg, followed by any number of arguments or keywords.
"""Instantiate a Call.
Pass a callable as the first arg, followed by any number of
arguments or keywords.
# Example: wrap a method call with 1 positional and 1 keyword arg:
mycall = ba.Call(myobj.dostuff, argval1, namedarg=argval2)
@ -259,3 +279,132 @@ class WeakMethod:
def __str__(self) -> str:
return '<ba.WeakMethod object; call=' + str(self._func) + '>'
def verify_object_death(obj: object) -> None:
"""Warn if an object does not get freed within a short period.
Category: General Utility Functions
This can be handy to detect and prevent memory/resource leaks.
"""
try:
ref = weakref.ref(obj)
except Exception:
print_exception('Unable to create weak-ref in verify_object_death')
# 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)
def print_active_refs(obj: Any) -> None:
"""Print info about things referencing a given object.
Category: General Utility Functions
Useful for tracking down cyclical references and causes for zombie objects.
"""
# pylint: disable=too-many-nested-blocks
from types import FrameType, TracebackType
refs = list(gc.get_referrers(obj))
print(f'{Clr.YLW}Active referrers to {obj}:{Clr.RST}')
for i, ref in enumerate(refs):
print(f'{Clr.YLW}#{i+1}:{Clr.BLU} {ref}{Clr.RST}')
# For certain types of objects such as stack frames, show what is
# keeping *them* alive too.
if isinstance(ref, FrameType):
print(f'{Clr.YLW} Active referrers to #{i+1}:{Clr.RST}')
refs2 = list(gc.get_referrers(ref))
for j, ref2 in enumerate(refs2):
print(f'{Clr.YLW} #a{j+1}:{Clr.BLU} {ref2}{Clr.RST}')
# Can go further down the rabbit-hole if needed...
if bool(False):
if isinstance(ref2, TracebackType):
print(f'{Clr.YLW} '
f'Active referrers to #a{j+1}:{Clr.RST}')
refs3 = list(gc.get_referrers(ref2))
for k, ref3 in enumerate(refs3):
print(f'{Clr.YLW} '
f'#b{k+1}:{Clr.BLU} {ref3}{Clr.RST}')
if isinstance(ref3, BaseException):
print(f'{Clr.YLW} Active referrers to'
f' #b{k+1}:{Clr.RST}')
refs4 = list(gc.get_referrers(ref3))
for x, ref4 in enumerate(refs4):
print(f'{Clr.YLW} #c{x+1}:{Clr.BLU}'
f' {ref4}{Clr.RST}')
def _verify_object_death(wref: ReferenceType) -> None:
obj = wref()
if obj is None:
return
try:
name = type(obj).__name__
except Exception:
print(f'Note: unable to get type name for {obj}')
name = 'object'
print(f'{Clr.RED}Error: {name} not dying when expected to:'
f' {Clr.BLD}{obj}{Clr.RST}')
print_active_refs(obj)
def storagename(suffix: str = None) -> str:
"""Generate a unique name for storing class data in shared places.
Category: General Utility Functions
This consists of a leading underscore, the module path at the
call site with dots replaced by underscores, the containing class's
qualified name, and the provided suffix. When storing data in public
places such as 'customdata' dicts, this minimizes the chance of
collisions with other similarly named classes.
Note that this will function even if called in the class definition.
# Example: generate a unique name for storage purposes:
class MyThingie:
# This will give something like '_mymodule_submodule_mythingie_data'.
_STORENAME = ba.storagename('data')
# Use that name to store some data in the Activity we were passed.
def __init__(self, activity):
activity.customdata[self._STORENAME] = {}
"""
frame = inspect.currentframe()
if frame is None:
raise RuntimeError('Cannot get current stack frame.')
fback = frame.f_back
# Note: We need to explicitly clear frame here to avoid a ref-loop
# that keeps all function-dicts in the stack alive until the next
# full GC cycle (the stack frame refers to this function's dict,
# which refers to the stack frame).
del frame
if fback is None:
raise RuntimeError('Cannot get parent stack frame.')
modulepath = fback.f_globals.get('__name__')
if modulepath is None:
raise RuntimeError('Cannot get parent stack module path.')
assert isinstance(modulepath, str)
qualname = fback.f_locals.get('__qualname__')
if qualname is not None:
assert isinstance(qualname, str)
fullpath = f'_{modulepath}_{qualname.lower()}'
else:
fullpath = f'_{modulepath}'
if suffix is not None:
fullpath = f'{fullpath}_{suffix}'
return fullpath.replace('.', '_')

View File

@ -1,31 +1,13 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Snippets of code for use by the internal C++ layer.
History: originally I would dynamically compile/eval bits of Python text
from within C++ code, but the major downside there was that I would
never catch code breakage until the code was next run. By defining all
snippets I use here and then capturing references to them all at launch
I can verify everything I'm looking for exists and pylint can do
its magic on this file.
from within C++ code, but the major downside there was that none of that was
type-checked so if names or arguments changed I would never catch code breakage
until the code was next run. By defining all snippets I use here and then
capturing references to them all at launch I can immediately verify everything
I'm looking for exists and pylint/mypy can do their magic on this file.
"""
# (most of these are self-explanatory)
# pylint: disable=missing-function-docstring
@ -36,7 +18,7 @@ from typing import TYPE_CHECKING
import _ba
if TYPE_CHECKING:
from typing import List, Sequence, Optional
from typing import List, Sequence, Optional, Dict, Any
import ba
@ -58,31 +40,31 @@ def set_config_fullscreen_off() -> None:
def not_signed_in_screen_message() -> None:
from ba._lang import Lstr
from ba._language import Lstr
_ba.screenmessage(Lstr(resource='notSignedInErrorText'))
def connecting_to_party_message() -> None:
from ba._lang import Lstr
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._lang import Lstr
from ba._language import Lstr
_ba.screenmessage(
Lstr(resource='internal.rejectingInviteAlreadyInPartyText'),
color=(1, 0.5, 0))
def connection_failed_message() -> None:
from ba._lang import Lstr
from ba._language import Lstr
_ba.screenmessage(Lstr(resource='internal.connectionFailedText'),
color=(1, 0.5, 0))
def temporarily_unavailable_message() -> None:
from ba._lang import Lstr
from ba._language import Lstr
_ba.playsound(_ba.getsound('error'))
_ba.screenmessage(
Lstr(resource='getTicketsWindow.unavailableTemporarilyText'),
@ -90,20 +72,20 @@ def temporarily_unavailable_message() -> None:
def in_progress_message() -> None:
from ba._lang import Lstr
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._lang import Lstr
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._lang import Lstr
from ba._language import Lstr
_ba.playsound(_ba.getsound('error'))
_ba.screenmessage(Lstr(resource='store.purchaseNotValidError',
subs=[('${EMAIL}', 'support@froemling.net')]),
@ -111,28 +93,28 @@ def purchase_not_valid_error() -> None:
def purchase_already_in_progress_error() -> None:
from ba._lang import Lstr
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._lang import Lstr
from ba._language import Lstr
_ba.playsound(_ba.getsound('error'))
_ba.screenmessage(Lstr(resource='usesExternalControllerText'),
color=(1, 0, 0))
def orientation_reset_cb_message() -> None:
from ba._lang import Lstr
from ba._language import Lstr
_ba.screenmessage(
Lstr(resource='internal.vrOrientationResetCardboardText'),
color=(0, 1, 0))
def orientation_reset_message() -> None:
from ba._lang import Lstr
from ba._language import Lstr
_ba.screenmessage(Lstr(resource='internal.vrOrientationResetText'),
color=(0, 1, 0))
@ -151,18 +133,16 @@ def launch_main_menu_session() -> None:
def language_test_toggle() -> None:
from ba._lang import setlanguage
setlanguage('Gibberish' if _ba.app.language == 'English' else 'English')
_ba.app.lang.setlanguage('Gibberish' if _ba.app.lang.language ==
'English' else 'English')
def award_in_control_achievement() -> None:
from ba._achievement import award_local_achievement
award_local_achievement('In Control')
_ba.app.ach.award_local_achievement('In Control')
def award_dual_wielding_achievement() -> None:
from ba._achievement import award_local_achievement
award_local_achievement('Dual Wielding')
_ba.app.ach.award_local_achievement('Dual Wielding')
def play_gong_sound() -> None:
@ -174,19 +154,19 @@ def launch_coop_game(name: str) -> None:
def purchases_restored_message() -> None:
from ba._lang import Lstr
from ba._language import Lstr
_ba.screenmessage(Lstr(resource='getTicketsWindow.purchasesRestoredText'),
color=(0, 1, 0))
def dismiss_wii_remotes_window() -> None:
call = _ba.app.dismiss_wii_remotes_window_call
call = _ba.app.ui.dismiss_wii_remotes_window_call
if call is not None:
call()
def unavailable_message() -> None:
from ba._lang import Lstr
from ba._language import Lstr
_ba.screenmessage(Lstr(resource='getTicketsWindow.unavailableText'),
color=(1, 0, 0))
@ -198,12 +178,12 @@ def submit_analytics_counts(sval: str) -> None:
def set_last_ad_network(sval: str) -> None:
import time
_ba.app.last_ad_network = sval
_ba.app.last_ad_network_set_time = time.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._lang import Lstr
from ba._language import Lstr
_ba.screenmessage(Lstr(resource='noGameCircleText'), color=(1, 0, 0))
@ -256,10 +236,10 @@ def party_icon_activate(origin: Sequence[float]) -> None:
_ba.playsound(_ba.getsound('swish'))
# If it exists, dismiss it; otherwise make a new one.
if app.party_window is not None and app.party_window() is not None:
app.party_window().close()
if app.ui.party_window is not None and app.ui.party_window() is not None:
app.ui.party_window().close()
else:
app.party_window = weakref.ref(PartyWindow(origin=origin))
app.ui.party_window = weakref.ref(PartyWindow(origin=origin))
def read_config() -> None:
@ -268,10 +248,13 @@ def read_config() -> None:
def ui_remote_press() -> None:
"""Handle a press by a remote device that is only usable for nav."""
from ba._lang import Lstr
_ba.screenmessage(Lstr(resource='internal.controllerForMenusOnlyText'),
color=(1, 0, 0))
_ba.playsound(_ba.getsound('error'))
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:
@ -280,7 +263,7 @@ def quit_window() -> None:
def remove_in_game_ads_message() -> None:
_ba.app.do_remove_in_game_ads_message()
_ba.app.ads.do_remove_in_game_ads_message()
def telnet_access_request() -> None:
@ -293,7 +276,7 @@ def do_quit() -> None:
def shutdown() -> None:
_ba.app.shutdown()
_ba.app.on_app_shutdown()
def gc_disable() -> None:
@ -303,11 +286,11 @@ def gc_disable() -> None:
def device_menu_press(device: ba.InputDevice) -> None:
from bastd.ui.mainmenu import MainMenuWindow
in_main_menu = bool(_ba.app.main_menu_window)
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.main_menu_window = (MainMenuWindow().get_root_widget())
_ba.app.ui.set_main_menu_window(MainMenuWindow().get_root_widget())
def show_url_window(address: str) -> None:
@ -338,11 +321,16 @@ def filter_chat_message(msg: str, client_id: int) -> Optional[str]:
def local_chat_message(msg: str) -> None:
if (_ba.app.party_window is not None
and _ba.app.party_window() is not None):
_ba.app.party_window().on_chat_message(msg)
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 handle_remote_achievement_list(completed_achievements: List[str]) -> None:
from ba import _achievement
_achievement.set_completed_achievements(completed_achievements)
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']
}

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Input related functionality"""
from __future__ import annotations
@ -45,11 +27,11 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
useragentstring = app.user_agent_string
platform = app.platform
subplatform = app.subplatform
bs_config = _ba.app.config
appconfig = _ba.app.config
# If there's an entry in our config for this controller, use it.
if 'Controllers' in bs_config:
ccfgs = bs_config['Controllers']
if 'Controllers' in appconfig:
ccfgs = appconfig['Controllers']
if devicename in ccfgs:
mapping = None
if unique_id in ccfgs[devicename]:
@ -562,8 +544,11 @@ def _gen_android_input_hash() -> str:
# (since it'll vary a lot across devices)
if f_name == 'gpio-keys.kl':
continue
with open(dirname + '/' + f_name, 'rb') as infile:
md5.update(infile.read())
try:
with open(f'{dirname}/{f_name}', 'rb') as infile:
md5.update(infile.read())
except PermissionError:
pass
except Exception:
from ba import _error
_error.print_exception(
@ -621,15 +606,15 @@ def get_last_player_name_from_input_device(device: ba.InputDevice) -> str:
(generally the last one used there)
"""
bs_config = _ba.app.config
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 bs_config
and key_name in bs_config['Default Player Profiles']):
profilename = bs_config['Default Player Profiles'][key_name]
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__':

View File

@ -0,0 +1,35 @@
# Released under the MIT License. See LICENSE for details.
#
"""On-screen Keyboard related functionality."""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import List, Tuple, Dict
class Keyboard:
"""Chars definitions for on-screen keyboard.
Category: App Classes
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.
Attributes:
name
Displays when user selecting this keyboard.
chars
Used for row/column lengths.
pages
Extra chars like emojis.
nums
The 'num' page.
"""
name: str
chars: List[Tuple[str, ...]]
pages: Dict[str, Tuple[str, ...]]
nums: Tuple[str, ...]

View File

@ -1,470 +0,0 @@
# Copyright (c) 2011-2020 Eric Froemling
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Language related functionality."""
from __future__ import annotations
import json
import os
from typing import TYPE_CHECKING, overload
import _ba
if TYPE_CHECKING:
from typing import Any, Dict, List, Optional, Tuple, Union, Sequence
class Lstr:
"""Used to define strings in a language-independent way.
category: General Utility Classes
These should be used whenever possible in place of hard-coded strings
so that in-game or UI elements show up correctly on all clients in their
currently-active language.
To see available resource keys, look at any of the bs_language_*.py files
in the game or the translations pages at bombsquadgame.com/translate.
# EXAMPLE 1: specify a string from a resource path
mynode.text = ba.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', '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}',
subs=[('${A}', str(score)), ('${B}', str(total))])
# EXAMPLE 4: Lstrs 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(resource='res_a',
subs=[('${NAME}', ba.Lstr(resource='res_b'))])
"""
# pylint: disable=redefined-outer-name, dangerous-default-value
# noinspection PyDefaultArgument
@overload
def __init__(self,
*,
resource: str,
fallback_resource: str = '',
fallback_value: str = '',
subs: Sequence[Tuple[str, Union[str, Lstr]]] = []) -> None:
"""Create an Lstr from a string resource."""
...
# noinspection PyShadowingNames,PyDefaultArgument
@overload
def __init__(self,
*,
translate: Tuple[str, str],
subs: Sequence[Tuple[str, Union[str, Lstr]]] = []) -> None:
"""Create an Lstr by translating a string in a category."""
...
# noinspection PyDefaultArgument
@overload
def __init__(self,
*,
value: str,
subs: Sequence[Tuple[str, Union[str, Lstr]]] = []) -> None:
"""Create an Lstr from a raw string value."""
...
# pylint: enable=redefined-outer-name, dangerous-default-value
def __init__(self, *args: Any, **keywds: Any) -> None:
"""Instantiate a Lstr.
Pass a value for either 'resource', 'translate',
or 'value'. (see Lstr help for examples).
'subs' can be a sequence of 2-member sequences consisting of values
and replacements.
'fallback_resource' can be a resource key that will be used if the
main one is not present for
the current language in place of falling back to the english value
('resource' mode only).
'fallback_value' can be a literal string that will be used if neither
the resource nor the fallback resource is found ('resource' mode only).
"""
# pylint: disable=too-many-branches
if args:
raise Exception('Lstr accepts only keyword arguments')
# Basically just store the exact args they passed.
# However if they passed any Lstr values for subs,
# replace them with that Lstr's dict.
self.args = keywds
our_type = type(self)
if isinstance(self.args.get('value'), our_type):
raise Exception("'value' must be a regular string; not an Lstr")
if 'subs' in self.args:
subs_new = []
for key, value in keywds['subs']:
if isinstance(value, our_type):
subs_new.append((key, value.args))
else:
subs_new.append((key, value))
self.args['subs'] = subs_new
# As of protocol 31 we support compact key names
# ('t' instead of 'translate', etc). Convert as needed.
if 'translate' in keywds:
keywds['t'] = keywds['translate']
del keywds['translate']
if 'resource' in keywds:
keywds['r'] = keywds['resource']
del keywds['resource']
if 'value' in keywds:
keywds['v'] = keywds['value']
del keywds['value']
if 'fallback' in keywds:
from ba import _error
_error.print_error(
'deprecated "fallback" arg passed to Lstr(); use '
'either "fallback_resource" or "fallback_value"',
once=True)
keywds['f'] = keywds['fallback']
del keywds['fallback']
if 'fallback_resource' in keywds:
keywds['f'] = keywds['fallback_resource']
del keywds['fallback_resource']
if 'subs' in keywds:
keywds['s'] = keywds['subs']
del keywds['subs']
if 'fallback_value' in keywds:
keywds['fv'] = keywds['fallback_value']
del keywds['fallback_value']
def evaluate(self) -> str:
"""Evaluate the Lstr and returns a flat string in the current language.
You should avoid doing this as much as possible and instead pass
and store Lstr values.
"""
return _ba.evaluate_lstr(self._get_json())
def is_flat_value(self) -> bool:
"""Return whether the Lstr is a 'flat' value.
This is defined as a simple string value incorporating no translations,
resources, or substitutions. In this case it may be reasonable to
replace it with a raw string value, perform string manipulation on it,
etc.
"""
return bool('v' in self.args and not self.args.get('s', []))
def _get_json(self) -> str:
try:
return json.dumps(self.args, separators=(',', ':'))
except Exception:
from ba import _error
_error.print_exception('_get_json failed for', self.args)
return 'JSON_ERR'
def __str__(self) -> str:
return '<ba.Lstr: ' + self._get_json() + '>'
def __repr__(self) -> str:
return '<ba.Lstr: ' + self._get_json() + '>'
def setlanguage(language: Optional[str],
print_change: bool = True,
store_to_config: bool = True) -> None:
"""Set the active language used for the game.
category: General Utility Functions
Pass None to use OS default language.
"""
# pylint: disable=too-many-locals
# pylint: disable=too-many-branches
cfg = _ba.app.config
cur_language = cfg.get('Lang', None)
# Store this in the config if its changing.
if language != cur_language and store_to_config:
if language is None:
if 'Lang' in cfg:
del cfg['Lang'] # Clear it out for default.
else:
cfg['Lang'] = language
cfg.commit()
switched = True
else:
switched = False
with open('ba_data/data/languages/english.json') as infile:
lenglishvalues = json.loads(infile.read())
# None implies default.
if language is None:
language = _ba.app.default_language
try:
if language == 'English':
lmodvalues = None
else:
lmodfile = 'ba_data/data/languages/' + language.lower() + '.json'
with open(lmodfile) as infile:
lmodvalues = json.loads(infile.read())
except Exception:
from ba import _error
_error.print_exception('Exception importing language:', language)
_ba.screenmessage("Error setting language to '" + language +
"'; see log for details",
color=(1, 0, 0))
switched = False
lmodvalues = None
# Create an attrdict of *just* our target language.
_ba.app.language_target = AttrDict()
langtarget = _ba.app.language_target
assert langtarget is not None
_add_to_attr_dict(langtarget,
lmodvalues if lmodvalues is not None else lenglishvalues)
# Create an attrdict of our target language overlaid on our base (english).
languages = [lenglishvalues]
if lmodvalues is not None:
languages.append(lmodvalues)
lfull = AttrDict()
for lmod in languages:
_add_to_attr_dict(lfull, lmod)
_ba.app.language_merged = lfull
# Pass some keys/values in for low level code to use;
# start with everything in their 'internal' section.
internal_vals = [
v for v in list(lfull['internal'].items()) if isinstance(v[1], str)
]
# Cherry-pick various other values to include.
# (should probably get rid of the 'internal' section
# and do everything this way)
for value in [
'replayNameDefaultText', 'replayWriteErrorText',
'replayVersionErrorText', 'replayReadErrorText'
]:
internal_vals.append((value, lfull[value]))
internal_vals.append(
('axisText', lfull['configGamepadWindow']['axisText']))
lmerged = _ba.app.language_merged
assert lmerged is not None
random_names = [
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)
if switched and print_change:
_ba.screenmessage(Lstr(resource='languageSetText',
subs=[('${LANGUAGE}',
Lstr(translate=('languages', language)))
]),
color=(0, 1, 0))
def _add_to_attr_dict(dst: AttrDict, src: Dict) -> None:
for key, value in list(src.items()):
if isinstance(value, dict):
try:
dst_dict = dst[key]
except Exception:
dst_dict = dst[key] = AttrDict()
if not isinstance(dst_dict, AttrDict):
raise Exception("language key '" + key +
"' is defined both as a dict and value")
_add_to_attr_dict(dst_dict, value)
else:
if not isinstance(value, (float, int, bool, str, str, type(None))):
raise Exception("invalid value type for res '" + key + "': " +
str(type(value)))
dst[key] = value
class AttrDict(dict):
"""A dict that can be accessed with dot notation.
(so foo.bar is equivalent to foo['bar'])
"""
def __getattr__(self, attr: str) -> Any:
val = self[attr]
assert not isinstance(val, bytes)
return val
def __setattr__(self, attr: str, value: Any) -> None:
raise Exception()
def get_resource(resource: str,
fallback_resource: str = None,
fallback_value: Any = None) -> Any:
"""Return a translation resource by name."""
try:
# If we have no language set, go ahead and set it.
if _ba.app.language_merged is None:
language = _ba.app.language
try:
setlanguage(language,
print_change=False,
store_to_config=False)
except Exception:
from ba import _error
_error.print_exception('exception setting language to',
language)
# Try english as a fallback.
if language != 'English':
print('Resorting to fallback language (English)')
try:
setlanguage('English',
print_change=False,
store_to_config=False)
except Exception:
_error.print_exception(
'error setting language to english fallback')
# If they provided a fallback_resource value, try the
# target-language-only dict first and then fall back to trying the
# fallback_resource value in the merged dict.
if fallback_resource is not None:
try:
values = _ba.app.language_target
splits = resource.split('.')
dicts = splits[:-1]
key = splits[-1]
for dct in dicts:
assert values is not None
values = values[dct]
assert values is not None
val = values[key]
return val
except Exception:
# FIXME: Shouldn't we try the fallback resource in the merged
# dict AFTER we try the main resource in the merged dict?
try:
values = _ba.app.language_merged
splits = fallback_resource.split('.')
dicts = splits[:-1]
key = splits[-1]
for dct in dicts:
assert values is not None
values = values[dct]
assert values is not None
val = values[key]
return val
except Exception:
# If we got nothing for fallback_resource, default to the
# normal code which checks or primary value in the merge
# dict; there's a chance we can get an english value for
# it (which we weren't looking for the first time through).
pass
values = _ba.app.language_merged
splits = resource.split('.')
dicts = splits[:-1]
key = splits[-1]
for dct in dicts:
assert values is not None
values = values[dct]
assert values is not None
val = values[key]
return val
except Exception:
# 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.
if fallback_value is not None:
return fallback_value
raise Exception("resource not found: '" + resource + "'")
def translate(category: str,
strval: str,
raise_exceptions: bool = False,
print_errors: bool = False) -> str:
"""Translate a value (or return the value if no translation available)
Generally you should use ba.Lstr which handles dynamic translation,
as opposed to this which returns a flat string.
"""
try:
translated = get_resource('translations')[category][strval]
except Exception as exc:
if raise_exceptions:
raise
if print_errors:
print(('Translate error: category=\'' + category + '\' name=\'' +
strval + '\' exc=' + str(exc) + ''))
translated = None
translated_out: str
if translated is None:
translated_out = strval
else:
translated_out = translated
assert isinstance(translated_out, str)
return translated_out
def get_valid_languages() -> List[str]:
"""Return a list containing names of all available languages.
category: General Utility Functions
Languages that may be present but are not displayable on the running
version of the game are ignored.
"""
langs = set()
app = _ba.app
try:
names = os.listdir('ba_data/data/languages')
names = [n.replace('.json', '').capitalize() for n in names]
# FIXME: our simple capitalization fails on multi-word names;
# should handle this in a better way...
for i, name in enumerate(names):
if name == 'Chinesetraditional':
names[i] = 'ChineseTraditional'
except Exception:
from ba import _error
_error.print_exception()
names = []
for name in names:
if app.can_display_language(name):
langs.add(name)
return sorted(name for name in names if app.can_display_language(name))
def is_custom_unicode_char(char: str) -> bool:
"""Return whether a char is in the custom unicode range we use."""
assert isinstance(char, str)
if len(char) != 1:
raise Exception('Invalid Input; must be length 1')
return 0xE000 <= ord(char) <= 0xF8FF

View File

@ -0,0 +1,562 @@
# Released under the MIT License. See LICENSE for details.
#
"""Language related functionality."""
from __future__ import annotations
import json
import os
from typing import TYPE_CHECKING, overload
import _ba
if TYPE_CHECKING:
import ba
from typing import Any, Dict, List, Optional, Tuple, Union, Sequence
class LanguageSubsystem:
"""Wraps up language related app functionality.
Category: App Classes
To use this class, access the single instance of it at 'ba.app.lang'.
"""
def __init__(self) -> None:
self.language_target: Optional[AttrDict] = None
self.language_merged: Optional[AttrDict] = None
self.default_language = self._get_default_language()
def _can_display_language(self, language: str) -> bool:
"""Tell whether we can display a particular language.
On some platforms we don't have unicode rendering yet
which limits the languages we can draw.
"""
# We don't yet support full unicode display on windows or linux :-(.
if (language in {
'Chinese', 'ChineseTraditional', 'Persian', 'Korean', 'Arabic',
'Hindi', 'Vietnamese'
} and not _ba.can_display_full_unicode()):
return False
return True
@property
def locale(self) -> str:
"""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
(which may differ from locale if the user sets a language, etc.)
"""
env = _ba.env()
assert isinstance(env['locale'], str)
return env['locale']
def _get_default_language(self) -> str:
languages = {
'de': 'German',
'es': 'Spanish',
'sk': 'Slovak',
'it': 'Italian',
'nl': 'Dutch',
'da': 'Danish',
'pt': 'Portuguese',
'fr': 'French',
'el': 'Greek',
'ru': 'Russian',
'pl': 'Polish',
'sv': 'Swedish',
'eo': 'Esperanto',
'cs': 'Czech',
'hr': 'Croatian',
'hu': 'Hungarian',
'be': 'Belarussian',
'ro': 'Romanian',
'ko': 'Korean',
'fa': 'Persian',
'ar': 'Arabic',
'zh': 'Chinese',
'tr': 'Turkish',
'id': 'Indonesian',
'sr': 'Serbian',
'uk': 'Ukrainian',
'vi': 'Vietnamese',
'vec': 'Venetian',
'hi': 'Hindi'
}
# Special case for Chinese: map specific variations to traditional.
# (otherwise will map to 'Chinese' which is simplified)
if self.locale in ('zh_HANT', 'zh_TW'):
language = 'ChineseTraditional'
else:
language = languages.get(self.locale[:2], 'English')
if not self._can_display_language(language):
language = 'English'
return language
@property
def language(self) -> str:
"""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.
"""
assert isinstance(_ba.app.config, dict)
return _ba.app.config.get('Lang', self.default_language)
@property
def available_languages(self) -> List[str]:
"""A list of all available languages.
Note that languages that may be present in game assets but which
are not displayable on the running version of the game are not
included here.
"""
langs = set()
try:
names = os.listdir('ba_data/data/languages')
names = [n.replace('.json', '').capitalize() for n in names]
# FIXME: our simple capitalization fails on multi-word names;
# should handle this in a better way...
for i, name in enumerate(names):
if name == 'Chinesetraditional':
names[i] = 'ChineseTraditional'
except Exception:
from ba import _error
_error.print_exception()
names = []
for name in names:
if self._can_display_language(name):
langs.add(name)
return sorted(name for name in names
if self._can_display_language(name))
def setlanguage(self,
language: Optional[str],
print_change: bool = True,
store_to_config: bool = True) -> None:
"""Set the active language used for the game.
Pass None to use OS default language.
"""
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
cfg = _ba.app.config
cur_language = cfg.get('Lang', None)
# Store this in the config if its changing.
if language != cur_language and store_to_config:
if language is None:
if 'Lang' in cfg:
del cfg['Lang'] # Clear it out for default.
else:
cfg['Lang'] = language
cfg.commit()
switched = True
else:
switched = False
with open('ba_data/data/languages/english.json') as infile:
lenglishvalues = json.loads(infile.read())
# None implies default.
if language is None:
language = self.default_language
try:
if language == 'English':
lmodvalues = None
else:
lmodfile = 'ba_data/data/languages/' + language.lower(
) + '.json'
with open(lmodfile) as infile:
lmodvalues = json.loads(infile.read())
except Exception:
from ba import _error
_error.print_exception('Exception importing language:', language)
_ba.screenmessage("Error setting language to '" + language +
"'; see log for details",
color=(1, 0, 0))
switched = False
lmodvalues = None
# Create an attrdict of *just* our target language.
self.language_target = AttrDict()
langtarget = self.language_target
assert langtarget is not None
_add_to_attr_dict(
langtarget,
lmodvalues if lmodvalues is not None else lenglishvalues)
# Create an attrdict of our target language overlaid
# on our base (english).
languages = [lenglishvalues]
if lmodvalues is not None:
languages.append(lmodvalues)
lfull = AttrDict()
for lmod in languages:
_add_to_attr_dict(lfull, lmod)
self.language_merged = lfull
# Pass some keys/values in for low level code to use;
# start with everything in their 'internal' section.
internal_vals = [
v for v in list(lfull['internal'].items())
if isinstance(v[1], str)
]
# Cherry-pick various other values to include.
# (should probably get rid of the 'internal' section
# and do everything this way)
for value in [
'replayNameDefaultText', 'replayWriteErrorText',
'replayVersionErrorText', 'replayReadErrorText'
]:
internal_vals.append((value, lfull[value]))
internal_vals.append(
('axisText', lfull['configGamepadWindow']['axisText']))
internal_vals.append(('buttonText', lfull['buttonText']))
lmerged = self.language_merged
assert lmerged is not None
random_names = [
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)
if switched and print_change:
_ba.screenmessage(Lstr(resource='languageSetText',
subs=[('${LANGUAGE}',
Lstr(translate=('languages',
language)))]),
color=(0, 1, 0))
def get_resource(self,
resource: str,
fallback_resource: str = None,
fallback_value: Any = None) -> Any:
"""Return a translation resource by name.
DEPRECATED; use ba.Lstr functionality for these purposes.
"""
try:
# If we have no language set, go ahead and set it.
if self.language_merged is None:
language = self.language
try:
self.setlanguage(language,
print_change=False,
store_to_config=False)
except Exception:
from ba import _error
_error.print_exception('exception setting language to',
language)
# Try english as a fallback.
if language != 'English':
print('Resorting to fallback language (English)')
try:
self.setlanguage('English',
print_change=False,
store_to_config=False)
except Exception:
_error.print_exception(
'error setting language to english fallback')
# If they provided a fallback_resource value, try the
# target-language-only dict first and then fall back to trying the
# fallback_resource value in the merged dict.
if fallback_resource is not None:
try:
values = self.language_target
splits = resource.split('.')
dicts = splits[:-1]
key = splits[-1]
for dct in dicts:
assert values is not None
values = values[dct]
assert values is not None
val = values[key]
return val
except Exception:
# FIXME: Shouldn't we try the fallback resource in the
# merged dict AFTER we try the main resource in the
# merged dict?
try:
values = self.language_merged
splits = fallback_resource.split('.')
dicts = splits[:-1]
key = splits[-1]
for dct in dicts:
assert values is not None
values = values[dct]
assert values is not None
val = values[key]
return val
except Exception:
# If we got nothing for fallback_resource, default
# to the normal code which checks or primary
# value in the merge dict; there's a chance we can
# get an english value for it (which we weren't
# looking for the first time through).
pass
values = self.language_merged
splits = resource.split('.')
dicts = splits[:-1]
key = splits[-1]
for dct in dicts:
assert values is not None
values = values[dct]
assert values is not None
val = values[key]
return val
except Exception:
# 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
if fallback_value is not None:
return fallback_value
raise _error.NotFoundError(
f"Resource not found: '{resource}'") from None
def translate(self,
category: str,
strval: str,
raise_exceptions: bool = False,
print_errors: bool = False) -> str:
"""Translate a value (or return the value if no translation available)
DEPRECATED; use ba.Lstr functionality for these purposes.
"""
try:
translated = self.get_resource('translations')[category][strval]
except Exception as exc:
if raise_exceptions:
raise
if print_errors:
print(('Translate error: category=\'' + category +
'\' name=\'' + strval + '\' exc=' + str(exc) + ''))
translated = None
translated_out: str
if translated is None:
translated_out = strval
else:
translated_out = translated
assert isinstance(translated_out, str)
return translated_out
def is_custom_unicode_char(self, char: str) -> bool:
"""Return whether a char is in the custom unicode range we use."""
assert isinstance(char, str)
if len(char) != 1:
raise ValueError('Invalid Input; must be length 1')
return 0xE000 <= ord(char) <= 0xF8FF
class Lstr:
"""Used to define strings in a language-independent way.
category: General Utility Classes
These should be used whenever possible in place of hard-coded strings
so that in-game or UI elements show up correctly on all clients in their
currently-active language.
To see available resource keys, look at any of the bs_language_*.py files
in the game or the translations pages at bombsquadgame.com/translate.
# EXAMPLE 1: specify a string from a resource path
mynode.text = ba.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', '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}',
subs=[('${A}', str(score)), ('${B}', str(total))])
# EXAMPLE 4: Lstrs 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(resource='res_a',
subs=[('${NAME}', ba.Lstr(resource='res_b'))])
"""
# pylint: disable=dangerous-default-value
# noinspection PyDefaultArgument
@overload
def __init__(self,
*,
resource: str,
fallback_resource: str = '',
fallback_value: str = '',
subs: Sequence[Tuple[str, Union[str, Lstr]]] = []) -> None:
"""Create an Lstr from a string resource."""
...
# noinspection PyShadowingNames,PyDefaultArgument
@overload
def __init__(self,
*,
translate: Tuple[str, str],
subs: Sequence[Tuple[str, Union[str, Lstr]]] = []) -> None:
"""Create an Lstr by translating a string in a category."""
...
# noinspection PyDefaultArgument
@overload
def __init__(self,
*,
value: str,
subs: Sequence[Tuple[str, Union[str, Lstr]]] = []) -> None:
"""Create an Lstr from a raw string value."""
...
# pylint: enable=redefined-outer-name, dangerous-default-value
def __init__(self, *args: Any, **keywds: Any) -> None:
"""Instantiate a Lstr.
Pass a value for either 'resource', 'translate',
or 'value'. (see Lstr help for examples).
'subs' can be a sequence of 2-member sequences consisting of values
and replacements.
'fallback_resource' can be a resource key that will be used if the
main one is not present for
the current language in place of falling back to the english value
('resource' mode only).
'fallback_value' can be a literal string that will be used if neither
the resource nor the fallback resource is found ('resource' mode only).
"""
# pylint: disable=too-many-branches
if args:
raise TypeError('Lstr accepts only keyword arguments')
# Basically just store the exact args they passed.
# However if they passed any Lstr values for subs,
# replace them with that Lstr's dict.
self.args = keywds
our_type = type(self)
if isinstance(self.args.get('value'), our_type):
raise TypeError("'value' must be a regular string; not an Lstr")
if 'subs' in self.args:
subs_new = []
for key, value in keywds['subs']:
if isinstance(value, our_type):
subs_new.append((key, value.args))
else:
subs_new.append((key, value))
self.args['subs'] = subs_new
# As of protocol 31 we support compact key names
# ('t' instead of 'translate', etc). Convert as needed.
if 'translate' in keywds:
keywds['t'] = keywds['translate']
del keywds['translate']
if 'resource' in keywds:
keywds['r'] = keywds['resource']
del keywds['resource']
if 'value' in keywds:
keywds['v'] = keywds['value']
del keywds['value']
if 'fallback' in keywds:
from ba import _error
_error.print_error(
'deprecated "fallback" arg passed to Lstr(); use '
'either "fallback_resource" or "fallback_value"',
once=True)
keywds['f'] = keywds['fallback']
del keywds['fallback']
if 'fallback_resource' in keywds:
keywds['f'] = keywds['fallback_resource']
del keywds['fallback_resource']
if 'subs' in keywds:
keywds['s'] = keywds['subs']
del keywds['subs']
if 'fallback_value' in keywds:
keywds['fv'] = keywds['fallback_value']
del keywds['fallback_value']
def evaluate(self) -> str:
"""Evaluate the Lstr and returns a flat string in the current language.
You should avoid doing this as much as possible and instead pass
and store Lstr values.
"""
return _ba.evaluate_lstr(self._get_json())
def is_flat_value(self) -> bool:
"""Return whether the Lstr is a 'flat' value.
This is defined as a simple string value incorporating no translations,
resources, or substitutions. In this case it may be reasonable to
replace it with a raw string value, perform string manipulation on it,
etc.
"""
return bool('v' in self.args and not self.args.get('s', []))
def _get_json(self) -> str:
try:
return json.dumps(self.args, separators=(',', ':'))
except Exception:
from ba import _error
_error.print_exception('_get_json failed for', self.args)
return 'JSON_ERR'
def __str__(self) -> str:
return '<ba.Lstr: ' + self._get_json() + '>'
def __repr__(self) -> str:
return '<ba.Lstr: ' + self._get_json() + '>'
@staticmethod
def from_json(json_string: str) -> ba.Lstr:
"""Given a json string, returns a ba.Lstr. Does no data validation."""
lstr = Lstr(value='')
lstr.args = json.loads(json_string)
return lstr
def _add_to_attr_dict(dst: AttrDict, src: Dict) -> None:
for key, value in list(src.items()):
if isinstance(value, dict):
try:
dst_dict = dst[key]
except Exception:
dst_dict = dst[key] = AttrDict()
if not isinstance(dst_dict, AttrDict):
raise RuntimeError("language key '" + key +
"' is defined both as a dict and value")
_add_to_attr_dict(dst_dict, value)
else:
if not isinstance(value, (float, int, bool, str, str, type(None))):
raise TypeError("invalid value type for res '" + key + "': " +
str(type(value)))
dst[key] = value
class AttrDict(dict):
"""A dict that can be accessed with dot notation.
(so foo.bar is equivalent to foo['bar'])
"""
def __getattr__(self, attr: str) -> Any:
val = self[attr]
assert not isinstance(val, bytes)
return val
def __setattr__(self, attr: str, value: Any) -> None:
raise Exception()

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality related to individual levels in a campaign."""
from __future__ import annotations
@ -42,10 +24,9 @@ class Level:
def __init__(self,
name: str,
gametype: Type[ba.GameActivity],
settings: Dict[str, Any],
settings: dict,
preview_texture_name: str,
displayname: str = None):
"""Initializes a Level object with the provided values."""
self._name = name
self._gametype = gametype
self._settings = settings
@ -81,8 +62,8 @@ class Level:
@property
def displayname(self) -> ba.Lstr:
"""The localized name for this Level."""
from ba import _lang
return _lang.Lstr(
from ba import _language
return _language.Lstr(
translate=('coopLevelNames', self._displayname
if self._displayname is not None else self._name),
subs=[('${GAME}',
@ -93,8 +74,9 @@ class Level:
"""The type of game used for this Level."""
return self._gametype
def get_campaign(self) -> Optional[ba.Campaign]:
"""Return the ba.Campaign this Level is associated with, or None."""
@property
def campaign(self) -> Optional[ba.Campaign]:
"""The ba.Campaign this Level is associated with, or None."""
return None if self._campaign is None else self._campaign()
@property
@ -123,7 +105,7 @@ class Level:
config = self._get_config_dict()
config['Complete'] = val
def get_high_scores(self) -> Dict:
def get_high_scores(self) -> dict:
"""Return the current high scores for this Level."""
config = self._get_config_dict()
high_scores_key = 'High Scores' + self.get_score_version_string()
@ -144,8 +126,7 @@ class Level:
can be changed to separate its new high score lists/etc. from the old.
"""
if self._score_version_string is None:
scorever = (
self._gametype.get_resolved_score_info()['score_version'])
scorever = self._gametype.getscoreconfig().version
if scorever != '':
scorever = ' ' + scorever
self._score_version_string = scorever
@ -168,15 +149,14 @@ class Level:
The referenced dict exists under the game's config dict and
can be modified in place."""
campaign = self.get_campaign()
campaign = self.campaign
if campaign is None:
raise Exception('level is not in a campaign')
campaign_config = campaign.get_config_dict()
val: Dict[str,
Any] = campaign_config.setdefault(self._name, {
'Rating': 0.0,
'Complete': False
})
raise RuntimeError('Level is not in a campaign.')
configdict = campaign.configdict
val: Dict[str, Any] = configdict.setdefault(self._name, {
'Rating': 0.0,
'Complete': False
})
assert isinstance(val, dict)
return val

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Map related functionality."""
from __future__ import annotations
@ -66,8 +48,8 @@ def get_map_display_string(name: str) -> ba.Lstr:
Category: Asset Functions
"""
from ba import _lang
return _lang.Lstr(translate=('mapsNames', name))
from ba import _language
return _language.Lstr(translate=('mapsNames', name))
def getmaps(playtype: str) -> List[str]:
@ -126,7 +108,7 @@ def get_unowned_maps() -> List[str]:
"""
from ba import _store
unowned_maps: Set[str] = set()
if not _ba.app.headless_build:
if not _ba.app.headless_mode:
for map_section in _store.get_store_layout()['maps']:
for mapitem in map_section['items']:
if not _ba.get_purchased(mapitem):
@ -143,8 +125,9 @@ def get_map_class(name: str) -> Type[ba.Map]:
name = get_filtered_map_name(name)
try:
return _ba.app.maps[name]
except Exception:
raise Exception("Map not found: '" + name + "'")
except KeyError:
from ba import _error
raise _error.NotFoundError(f"Map not found: '{name}'") from None
class Map(Actor):
@ -191,7 +174,7 @@ class Map(Actor):
return None
@classmethod
def get_name(cls) -> str:
def getname(cls) -> str:
"""Return the unique name of this map, in English."""
return cls.name
@ -217,25 +200,27 @@ class Map(Actor):
# (and instruct the user if we weren't preloaded properly).
try:
self.preloaddata = _ba.getactivity().preloads[type(self)]
except Exception:
raise Exception('Preload data not found for ' + str(type(self)) +
'; make sure to call the type\'s preload()'
' staticmethod in the activity constructor')
except Exception as exc:
from ba import _error
raise _error.NotFoundError(
'Preload data not found for ' + str(type(self)) +
'; make sure to call the type\'s preload()'
' staticmethod in the activity constructor') from exc
# Set various globals.
gnode = _gameutils.sharedobj('globals')
gnode = _ba.getactivity().globalsnode
# Set area-of-interest bounds.
aoi_bounds = self.get_def_bound_box('area_of_interest_bounds')
if aoi_bounds is None:
print('WARNING: no "aoi_bounds" found for map:', self.get_name())
print('WARNING: no "aoi_bounds" found for map:', self.getname())
aoi_bounds = (-1, -1, -1, 1, 1, 1)
gnode.area_of_interest_bounds = aoi_bounds
# Set map bounds.
map_bounds = self.get_def_bound_box('map_bounds')
if map_bounds is None:
print('WARNING: no "map_bounds" found for map:', self.get_name())
print('WARNING: no "map_bounds" found for map:', self.getname())
map_bounds = (-30, -10, -30, 30, 100, 30)
_ba.set_map_bounds(map_bounds)
@ -339,7 +324,7 @@ class Map(Actor):
point_list.append(pts)
else:
if len(pts) != 3:
raise Exception('invalid point')
raise ValueError('invalid point')
point_list.append(pts + (0, 0, 0))
i += 1
return point_list
@ -364,12 +349,8 @@ class Map(Actor):
# Get positions for existing players.
player_pts = []
for player in players:
try:
if player.node:
pnt = _ba.Vec3(player.node.position)
player_pts.append(pnt)
except Exception as exc:
print('EXC in get_ffa_start_position:', exc)
if player.is_alive():
player_pts.append(player.position)
def _getpt() -> Sequence[float]:
point = self.ffa_spawn_points[self._next_ffa_start_index]
@ -427,5 +408,5 @@ class Map(Actor):
def register_map(maptype: Type[Map]) -> None:
"""Register a map class with the game."""
if maptype.name in _ba.app.maps:
raise Exception('map "' + maptype.name + '" already registered')
raise RuntimeError('map "' + maptype.name + '" already registered')
_ba.app.maps[maptype.name] = maptype

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Math related functionality."""
from __future__ import annotations

View File

@ -1,38 +1,30 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Defines some standard message objects for use with handlemessage() calls."""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, TypeVar
from enum import Enum
import _ba
if TYPE_CHECKING:
from typing import Sequence
from typing import Sequence, Optional, Type, Any
import ba
class _UnhandledType:
pass
# A special value that should be returned from handlemessage()
# functions for unhandled message types. This may result
# in fallback message types being attempted/etc.
UNHANDLED = _UnhandledType()
@dataclass
class OutOfBoundsMessage:
"""A message telling an object that it is out of bounds.
@ -78,6 +70,64 @@ class DieMessage:
how: DeathType = DeathType.GENERIC
PlayerType = TypeVar('PlayerType', bound='ba.Player')
class PlayerDiedMessage:
"""A message saying a ba.Player has died.
category: Message Classes
Attributes:
killed
If True, the player was killed;
If False, they left the game or the round ended.
how
The particular type of death.
"""
killed: bool
how: ba.DeathType
def __init__(self, player: ba.Player, was_killed: bool,
killerplayer: Optional[ba.Player], how: ba.DeathType):
"""Instantiate a message with the given values."""
# Invalid refs should never be passed as args.
assert player.exists()
self._player = player
# Invalid refs should never be passed as args.
assert killerplayer is None or killerplayer.exists()
self._killerplayer = killerplayer
self.killed = was_killed
self.how = how
def getkillerplayer(self,
playertype: Type[PlayerType]) -> Optional[PlayerType]:
"""Return the ba.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.
The type of player for the current activity should be passed so that
the type-checker properly identifies the returned value as one.
"""
player: Any = self._player
assert isinstance(player, playertype)
# We should never be delivering invalid refs.
# (could theoretically happen if someone holds on to us)
assert player.exists()
return player
@dataclass
class StandMessage:
"""A message telling an object to move to a position in space.
@ -202,7 +252,6 @@ class CelebrateMessage:
duration: float = 10.0
@dataclass(init=False)
class HitMessage:
"""Tells an object it has been hit in some way.
@ -233,7 +282,10 @@ class HitMessage:
self.magnitude = magnitude
self.velocity_magnitude = velocity_magnitude
self.radius = radius
self.source_player = source_player
# We should not be getting passed an invalid ref.
assert source_player is None or source_player.exists()
self._source_player = source_player
self.kick_back = kick_back
self.flat_damage = flat_damage
self.hit_type = hit_type
@ -241,6 +293,19 @@ class HitMessage:
self.force_direction = (force_direction
if force_direction is not None else velocity)
def get_source_player(
self, playertype: Type[PlayerType]) -> Optional[PlayerType]:
"""Return the source-player if one exists and is the provided type."""
player: Any = self._source_player
# We should not be delivering invalid refs.
# (we could translate to None here but technically we are changing
# the message delivered which seems wrong)
assert player is None or player.exists()
# Return the player *only* if they're the type given.
return player if isinstance(player, playertype) else None
@dataclass
class PlayerProfilesChangedMessage:

View File

@ -1,28 +1,11 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality related to dynamic discoverability of classes."""
from __future__ import annotations
import os
import time
import pathlib
import threading
from typing import TYPE_CHECKING
@ -37,7 +20,7 @@ if TYPE_CHECKING:
# The meta api version of this build of the game.
# Only packages and modules requiring this exact api version
# will be considered when scanning directories.
# See: https://github.com/efroemling/ballistica/wiki/Meta-Tags
# See: https://ballistica.net/wiki/Meta-Tags
CURRENT_API_VERSION = 6
@ -45,39 +28,161 @@ CURRENT_API_VERSION = 6
class ScanResults:
"""Final results from a metadata scan."""
games: List[str] = field(default_factory=list)
plugins: List[str] = field(default_factory=list)
keyboards: List[str] = field(default_factory=list)
errors: str = ''
warnings: str = ''
def start_scan() -> None:
"""Begin scanning script directories for scripts containing metadata.
class MetadataSubsystem:
"""Subsystem for working with script metadata in the app.
Should be called only once at launch."""
app = _ba.app
if app.metascan is not None:
print('WARNING: meta scan run more than once.')
scriptdirs = [app.python_directory_ba, app.python_directory_user]
thread = ScanThread(scriptdirs)
thread.start()
Category: App Classes
Access the single shared instance of this class at 'ba.app.meta'.
"""
def handle_scan_results(results: ScanResults) -> None:
"""Called in the game thread with results of a completed scan."""
from ba import _lang
def __init__(self) -> None:
self.metascan: Optional[ScanResults] = None
# Warnings generally only get printed locally for users' benefit
# (things like out-of-date scripts being ignored, etc.)
# Errors are more serious and will get included in the regular log
# warnings = results.get('warnings', '')
# errors = results.get('errors', '')
if results.warnings != '' or results.errors != '':
_ba.screenmessage(_lang.Lstr(resource='scanScriptsErrorText'),
color=(1, 0, 0))
_ba.playsound(_ba.getsound('error'))
if results.warnings != '':
_ba.log(results.warnings, to_server=False)
if results.errors != '':
_ba.log(results.errors)
def on_app_launch(self) -> None:
"""Should be called when the app is done bootstrapping."""
# Start scanning for things exposed via ba_meta.
self.start_scan()
def start_scan(self) -> None:
"""Begin scanning script directories for scripts containing metadata.
Should be called only once at launch."""
app = _ba.app
if self.metascan is not None:
print('WARNING: meta scan run more than once.')
pythondirs = [app.python_directory_app, app.python_directory_user]
thread = ScanThread(pythondirs)
thread.start()
def handle_scan_results(self, results: ScanResults) -> None:
"""Called in the game thread with results of a completed scan."""
from ba._language import Lstr
from ba._plugin import PotentialPlugin
# Warnings generally only get printed locally for users' benefit
# (things like out-of-date scripts being ignored, etc.)
# Errors are more serious and will get included in the regular log
# warnings = results.get('warnings', '')
# errors = results.get('errors', '')
if results.warnings != '' or results.errors != '':
import textwrap
_ba.screenmessage(Lstr(resource='scanScriptsErrorText'),
color=(1, 0, 0))
_ba.playsound(_ba.getsound('error'))
if results.warnings != '':
_ba.log(textwrap.indent(results.warnings,
'Warning (meta-scan): '),
to_server=False)
if results.errors != '':
_ba.log(textwrap.indent(results.errors, 'Error (meta-scan): '))
# Handle plugins.
plugs = _ba.app.plugins
config_changed = False
found_new = False
plugstates: Dict[str, Dict] = _ba.app.config.setdefault('Plugins', {})
assert isinstance(plugstates, dict)
# Create a potential-plugin for each class we found in the scan.
for class_path in results.plugins:
plugs.potential_plugins.append(
PotentialPlugin(display_name=Lstr(value=class_path),
class_path=class_path,
available=True))
if class_path not in plugstates:
if _ba.app.headless_mode:
# If we running in headless mode, enable plugin by default
# to allow server admins to get their modified build
# working 'out-of-the-box', without manually updating the
# config.
plugstates[class_path] = {'enabled': True}
else:
# If we running in normal mode, disable plugin by default
# (user can enable it later).
plugstates[class_path] = {'enabled': False}
config_changed = True
found_new = True
# Also add a special one for any plugins set to load but *not* found
# in the scan (this way they will show up in the UI so we can disable
# them)
for class_path, plugstate in plugstates.items():
enabled = plugstate.get('enabled', False)
assert isinstance(enabled, bool)
if enabled and class_path not in results.plugins:
plugs.potential_plugins.append(
PotentialPlugin(display_name=Lstr(value=class_path),
class_path=class_path,
available=False))
plugs.potential_plugins.sort(key=lambda p: p.class_path)
if found_new:
_ba.screenmessage(Lstr(resource='pluginsDetectedText'),
color=(0, 1, 0))
_ba.playsound(_ba.getsound('ding'))
if config_changed:
_ba.app.config.commit()
def get_scan_results(self) -> ScanResults:
"""Return meta scan results; block if the scan is not yet complete."""
if self.metascan is None:
print('WARNING: ba.meta.get_scan_results()'
' called before scan completed.'
' This can cause hitches.')
# Now wait a bit for the scan to complete.
# Eventually error though if it doesn't.
starttime = time.time()
while self.metascan is None:
time.sleep(0.05)
if time.time() - starttime > 10.0:
raise TimeoutError(
'timeout waiting for meta scan to complete.')
return self.metascan
def get_game_types(self) -> List[Type[ba.GameActivity]]:
"""Return available game types."""
from ba._general import getclass
from ba._gameactivity import GameActivity
gameclassnames = self.get_scan_results().games
gameclasses = []
for gameclassname in gameclassnames:
try:
cls = getclass(gameclassname, GameActivity)
gameclasses.append(cls)
except Exception:
from ba import _error
_error.print_exception('error importing ' + str(gameclassname))
unowned = self.get_unowned_game_types()
return [cls for cls in gameclasses if cls not in unowned]
def get_unowned_game_types(self) -> Set[Type[ba.GameActivity]]:
"""Return present game types not owned by the current account."""
try:
from ba import _store
unowned_games: Set[Type[ba.GameActivity]] = set()
if not _ba.app.headless_mode:
for section in _store.get_store_layout()['minigames']:
for mname in section['items']:
if not _ba.get_purchased(mname):
m_info = _store.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()
class ScanThread(threading.Thread):
@ -88,7 +193,7 @@ class ScanThread(threading.Thread):
self._dirs = dirs
def run(self) -> None:
from ba import _general
from ba._general import Call
try:
scan = DirectoryScan(self._dirs)
scan.scan()
@ -98,13 +203,13 @@ class ScanThread(threading.Thread):
# Push a call to the game thread to print warnings/errors
# or otherwise deal with scan results.
_ba.pushcall(_general.Call(handle_scan_results, results),
_ba.pushcall(Call(_ba.app.meta.handle_scan_results, results),
from_other_thread=True)
# We also, however, immediately make results available.
# This is because the game thread may be blocked waiting
# for them so we can't push a call or we'd get deadlock.
_ba.app.metascan = results
_ba.app.meta.metascan = results
class DirectoryScan:
@ -115,16 +220,10 @@ class DirectoryScan:
It is assumed that these paths are also in PYTHONPATH.
It is also assumed that any subdirectories are Python packages.
The returned dict contains the following:
'powerups': list of ba.Powerup classes found.
'campaigns': list of ba.Campaign classes found.
'modifiers': list of ba.Modifier classes found.
'maps': list of ba.Map classes found.
'games': list of ba.GameActivity classes found.
'warnings': warnings from scan; should be printed for local feedback
'errors': errors encountered during scan; should be fully logged
"""
self.paths = [pathlib.Path(p) for p in paths]
# Skip non-existent paths completely.
self.paths = [pathlib.Path(p) for p in paths if os.path.isdir(p)]
self.results = ScanResults()
def _get_path_module_entries(
@ -166,6 +265,9 @@ class DirectoryScan:
self.results.warnings += ("Error scanning '" + str(subpath) +
"': " + traceback.format_exc() +
'\n')
# Sort our results
self.results.games.sort()
self.results.plugins.sort()
def scan_module(self, moduledir: pathlib.Path,
subpath: pathlib.Path) -> None:
@ -206,7 +308,8 @@ class DirectoryScan:
submodules: List[Tuple[pathlib.Path, pathlib.Path]] = []
self._get_path_module_entries(moduledir, subpath, submodules)
for submodule in submodules:
self.scan_module(submodule[0], submodule[1])
if submodule[1].name != '__init__.py':
self.scan_module(submodule[0], submodule[1])
except Exception:
import traceback
self.results.warnings += (
@ -247,6 +350,10 @@ class DirectoryScan:
classname = modulename + '.' + export_class_name
if exporttype == 'game':
self.results.games.append(classname)
elif exporttype == 'plugin':
self.results.plugins.append(classname)
elif exporttype == 'keyboard':
self.results.keyboards.append(classname)
else:
self.results.warnings += (
'Warning: ' + str(subpath) +
@ -271,7 +378,7 @@ class DirectoryScan:
cbits = lbits[1].split('(')
if len(cbits) > 1 and cbits[0].isidentifier():
classname = cbits[0]
break # success!
break # Success!
if classname is None:
self.results.warnings += (
'Warning: ' + str(subpath) + ': class definition not found'
@ -291,7 +398,7 @@ class DirectoryScan:
and l[1] == 'require' and l[2] == 'api' and l[3].isdigit()
]
# we're successful if we find exactly one properly formatted line
# We're successful if we find exactly one properly formatted line.
if len(lines) == 1:
return int(lines[0][3])
@ -309,57 +416,3 @@ class DirectoryScan:
': no valid "# ba_meta api require <NUM>" line found;'
' ignoring module.\n')
return None
def get_scan_results() -> ScanResults:
"""Return meta scan results; blocking if the scan is not yet complete."""
import time
app = _ba.app
if app.metascan is None:
print(
'WARNING: ba.meta.get_scan_results() called before scan completed.'
' This can cause hitches.')
# Now wait a bit for the scan to complete.
# Eventually error though if it doesn't.
starttime = time.time()
while app.metascan is None:
time.sleep(0.05)
if time.time() - starttime > 10.0:
raise Exception('timeout waiting for meta scan to complete.')
return app.metascan
def get_game_types() -> List[Type[ba.GameActivity]]:
"""Return available game types."""
from ba import _general
from ba import _gameactivity
gameclassnames = get_scan_results().games
gameclasses = []
for gameclassname in gameclassnames:
try:
cls = _general.getclass(gameclassname, _gameactivity.GameActivity)
gameclasses.append(cls)
except Exception:
from ba import _error
_error.print_exception('error importing ' + str(gameclassname))
unowned = get_unowned_game_types()
return [cls for cls in gameclasses if cls not in unowned]
def get_unowned_game_types() -> Set[Type[ba.GameActivity]]:
"""Return present game types not owned by the current account."""
try:
from ba import _store
unowned_games: Set[Type[ba.GameActivity]] = set()
if not _ba.app.headless_build:
for section in _store.get_store_layout()['minigames']:
for mname in section['items']:
if not _ba.get_purchased(mname):
m_info = _store.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()

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality related to teams sessions."""
from __future__ import annotations
@ -27,6 +9,7 @@ from typing import TYPE_CHECKING
import _ba
from ba._session import Session
from ba._error import NotFoundError, print_error
if TYPE_CHECKING:
from typing import Optional, Any, Dict, List, Type, Sequence
@ -58,7 +41,7 @@ class MultiTeamSession(Session):
app = _ba.app
cfg = app.config
if self._use_teams:
if self.use_teams:
team_names = cfg.get('Custom Team Names', DEFAULT_TEAM_NAMES)
team_colors = cfg.get('Custom Team Colors', DEFAULT_TEAM_COLORS)
else:
@ -71,7 +54,6 @@ class MultiTeamSession(Session):
super().__init__(depsets,
team_names=team_names,
team_colors=team_colors,
use_team_colors=self._use_teams,
min_players=1,
max_players=self.get_max_players())
@ -85,7 +67,7 @@ class MultiTeamSession(Session):
from bastd.tutorial import TutorialActivity
# Get this loading.
self._tutorial_activity_instance = _ba.new_activity(
self._tutorial_activity_instance = _ba.newactivity(
TutorialActivity)
else:
self._tutorial_activity_instance = None
@ -106,7 +88,7 @@ class MultiTeamSession(Session):
# got it and we don't want that to affect our config.
playlist = copy.deepcopy(playlists[self._playlist_name])
else:
if self._use_teams:
if self.use_teams:
playlist = _playlist.get_default_teams_playlist()
else:
playlist = _playlist.get_default_free_for_all_playlist()
@ -133,7 +115,7 @@ class MultiTeamSession(Session):
self._instantiate_next_game()
# Start in our custom join screen.
self.set_activity(_ba.new_activity(MultiTeamJoinActivity))
self.setactivity(_ba.newactivity(MultiTeamJoinActivity))
def get_ffa_series_length(self) -> int:
"""Return free-for-all series length."""
@ -149,60 +131,58 @@ class MultiTeamSession(Session):
from ba._gameactivity import GameActivity
gametype: Type[GameActivity] = self._next_game_spec['resolved_type']
assert issubclass(gametype, GameActivity)
return gametype.get_config_display_string(self._next_game_spec)
return gametype.get_settings_display_string(self._next_game_spec)
def get_game_number(self) -> int:
"""Returns which game in the series is currently being played."""
return self._game_number
def on_team_join(self, team: ba.Team) -> None:
team.sessiondata['previous_score'] = team.sessiondata['score'] = 0
def on_team_join(self, team: ba.SessionTeam) -> None:
team.customdata['previous_score'] = team.customdata['score'] = 0
def get_max_players(self) -> int:
"""Return max number of ba.Players allowed to join the game at once."""
if self._use_teams:
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)
def _instantiate_next_game(self) -> None:
self._next_game_instance = _ba.new_activity(
self._next_game_instance = _ba.newactivity(
self._next_game_spec['resolved_type'],
self._next_game_spec['settings'])
def on_activity_end(self, activity: ba.Activity, results: Any) -> None:
# pylint: disable=cyclic-import
from ba import _error
from bastd.tutorial import TutorialActivity
from bastd.activity.multiteamvictory import (
TeamSeriesVictoryScoreScreenActivity)
from ba import _activitytypes
from ba._activitytypes import (TransitionActivity, JoinActivity,
ScoreScreenActivity)
# If we have a tutorial to show, that's the first thing we do no
# matter what.
if self._tutorial_activity_instance is not None:
self.set_activity(self._tutorial_activity_instance)
self.setactivity(self._tutorial_activity_instance)
self._tutorial_activity_instance = None
# If we're leaving the tutorial activity, pop a transition activity
# to transition us into a round gracefully (otherwise we'd snap from
# one terrain to another instantly).
elif isinstance(activity, TutorialActivity):
self.set_activity(
_ba.new_activity(_activitytypes.TransitionActivity))
self.setactivity(_ba.newactivity(TransitionActivity))
# If we're in a between-round activity or a restart-activity, hop
# into a round.
elif isinstance(
activity,
(_activitytypes.JoinActivity, _activitytypes.TransitionActivity,
_activitytypes.ScoreScreenActivity)):
(JoinActivity, TransitionActivity, ScoreScreenActivity)):
# If we're coming from a series-end activity, reset scores.
if isinstance(activity, TeamSeriesVictoryScoreScreenActivity):
self.stats.reset()
self._game_number = 0
for team in self.teams:
team.sessiondata['score'] = 0
for team in self.sessionteams:
team.customdata['score'] = 0
# Otherwise just set accum (per-game) scores.
else:
@ -218,19 +198,19 @@ class MultiTeamSession(Session):
self._instantiate_next_game()
# (Re)register all players and wire stats to our next activity.
for player in self.players:
for player in self.sessionplayers:
# ..but only ones who have been placed on a team
# (ie: no longer sitting in the lobby).
try:
has_team = (player.team is not None)
except _error.TeamNotFoundError:
has_team = (player.sessionteam is not None)
except NotFoundError:
has_team = False
if has_team:
self.stats.register_player(player)
self.stats.set_activity(next_game)
self.stats.register_sessionplayer(player)
self.stats.setactivity(next_game)
# Now flip the current activity.
self.set_activity(next_game)
self.setactivity(next_game)
# If we're leaving a round, go to the score screen.
else:
@ -238,13 +218,12 @@ class MultiTeamSession(Session):
def _switch_to_score_screen(self, results: Any) -> None:
"""Switch to a score screen after leaving a round."""
from ba import _error
del results # Unused arg.
_error.print_error('this should be overridden')
print_error('this should be overridden')
def announce_game_results(self,
activity: ba.GameActivity,
results: ba.TeamGameResults,
results: ba.GameResults,
delay: float,
announce_winning_team: bool = True) -> None:
"""Show basic game result at the end of a game.
@ -256,20 +235,21 @@ class MultiTeamSession(Session):
"""
# pylint: disable=cyclic-import
# pylint: disable=too-many-locals
from ba import _math
from ba import _general
from ba._math import normalized_color
from ba._general import Call
from ba._gameutils import cameraflash
from ba import _lang
from ba._language import Lstr
from ba._freeforallsession import FreeForAllSession
from ba._messages import CelebrateMessage
_ba.timer(delay,
_general.Call(_ba.playsound, _ba.getsound('boxingBell')))
_ba.timer(delay, Call(_ba.playsound, _ba.getsound('boxingBell')))
if announce_winning_team:
winning_team = results.get_winning_team()
if winning_team is not None:
winning_sessionteam = results.winning_sessionteam
if winning_sessionteam is not None:
# Have all players celebrate.
celebrate_msg = CelebrateMessage(duration=10.0)
for player in winning_team.players:
assert winning_sessionteam.activityteam is not None
for player in winning_sessionteam.activityteam.players:
if player.actor:
player.actor.handlemessage(celebrate_msg)
cameraflash()
@ -279,12 +259,13 @@ class MultiTeamSession(Session):
wins_resource = 'winsPlayerText'
else:
wins_resource = 'winsTeamText'
wins_text = _lang.Lstr(resource=wins_resource,
subs=[('${NAME}', winning_team.name)])
activity.show_zoom_message(wins_text,
scale=0.85,
color=_math.normalized_color(
winning_team.color))
wins_text = Lstr(resource=wins_resource,
subs=[('${NAME}', winning_sessionteam.name)])
activity.show_zoom_message(
wins_text,
scale=0.85,
color=normalized_color(winning_sessionteam.color),
)
class ShuffleList:

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Music related functionality."""
from __future__ import annotations
@ -30,6 +12,7 @@ import _ba
if TYPE_CHECKING:
from typing import Callable, Any, Optional, Dict, Union, Type
import ba
class MusicType(Enum):
@ -134,10 +117,12 @@ ASSET_SOUNDTRACK_ENTRIES: Dict[MusicType, AssetSoundtrackEntry] = {
}
class MusicController:
"""Controller for overall music playback in the app.
class MusicSubsystem:
"""Subsystem for music playback in the app.
Category: App Classes
Access the single shared instance of this class at 'ba.app.music'.
"""
def __init__(self) -> None:
@ -191,7 +176,7 @@ class MusicController:
"""Returns the system music player, instantiating if necessary."""
if self._music_player is None:
if self._music_player_type is None:
raise Exception('no music player type set')
raise TypeError('no music player type set')
self._music_player = self._music_player_type()
return self._music_player
@ -248,20 +233,21 @@ class MusicController:
and isinstance(entry['name'], str)):
entry_type = entry['type']
else:
raise Exception('invalid soundtrack entry: ' + str(entry) +
raise TypeError('invalid soundtrack entry: ' + str(entry) +
' (type ' + str(type(entry)) + ')')
if self.supports_soundtrack_entry_type(entry_type):
return entry_type
raise Exception('invalid soundtrack entry:' + str(entry))
except Exception as exc:
print('EXC on get_soundtrack_entry_type', exc)
raise ValueError('invalid soundtrack entry:' + str(entry))
except Exception:
from ba import _error
_error.print_exception()
return 'default'
def get_soundtrack_entry_name(self, entry: Any) -> str:
"""Given a soundtrack entry, returns its name."""
try:
if entry is None:
raise Exception('entry is None')
raise TypeError('entry is None')
# Simple string denotes an iTunesPlaylist name (legacy entry).
if isinstance(entry, str):
@ -272,7 +258,7 @@ class MusicController:
and isinstance(entry['type'], str) and 'name' in entry
and isinstance(entry['name'], str)):
return entry['name']
raise Exception('invalid soundtrack entry:' + str(entry))
raise ValueError('invalid soundtrack entry:' + str(entry))
except Exception:
from ba import _error
_error.print_exception()
@ -484,8 +470,9 @@ class MusicPlayer:
self._actually_playing = False
def setmusic(musictype: Optional[MusicType], continuous: bool = False) -> None:
"""Tell the game to play (or stop playing) a certain type of music.
def setmusic(musictype: Optional[ba.MusicType],
continuous: bool = False) -> None:
"""Set the app to play (or stop playing) a certain type of music.
category: Gameplay Functions
@ -505,7 +492,7 @@ def setmusic(musictype: Optional[MusicType], continuous: bool = False) -> None:
# 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 = _gameutils.sharedobj('globals')
gnode = _ba.getactivity().globalsnode
gnode.music_continuous = continuous
gnode.music = '' if musictype is None else musictype.value
gnode.music_count += 1

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Networking related functionality."""
from __future__ import annotations
@ -56,7 +38,7 @@ def get_ip_address_type(addr: str) -> socket.AddressFamily:
except OSError:
pass
if socket_type is None:
raise Exception('addr seems to be neither v4 or v6: ' + str(addr))
raise ValueError(f'addr seems to be neither v4 or v6: {addr}')
return socket_type
@ -76,14 +58,14 @@ class ServerCallThread(threading.Thread):
self._request = request
self._request_type = request_type
if not isinstance(response_type, ServerResponseType):
raise Exception(f'Invalid response type: {response_type}')
raise TypeError(f'Invalid response type: {response_type}')
self._response_type = response_type
self._data = {} if data is None else copy.deepcopy(data)
self._callback: Optional[ServerCallbackType] = callback
self._context = _ba.Context('current')
# Save and restore the context we were created from.
activity: Optional[ba.Activity] = _ba.getactivity(doraise=False)
activity = _ba.getactivity(doraise=False)
self._activity = weakref.ref(
activity) if activity is not None else None
@ -94,7 +76,7 @@ class ServerCallThread(threading.Thread):
# this check manually?
if self._activity is not None:
activity = self._activity()
if activity is None or activity.is_expired():
if activity is None or activity.expired:
return
# Technically we could do the same check for session contexts,
@ -105,16 +87,15 @@ class ServerCallThread(threading.Thread):
self._callback(arg)
def run(self) -> None:
# pylint: disable=too-many-branches
import urllib.request
import urllib.error
import json
import http.client
from ba import _general
try:
self._data = _general.utf8_all(self._data)
_ba.set_thread_name('BA_ServerCallThread')
# Seems pycharm doesn't know about urllib.parse.
# noinspection PyUnresolvedReferences
parse = urllib.parse
if self._request_type == 'get':
response = urllib.request.urlopen(
@ -129,7 +110,7 @@ class ServerCallThread(threading.Thread):
parse.urlencode(self._data).encode(),
{'User-Agent': _ba.app.user_agent_string}))
else:
raise Exception('Invalid request_type: ' + self._request_type)
raise TypeError('Invalid request_type: ' + self._request_type)
# If html request failed.
if response.getcode() != 200:
@ -145,23 +126,49 @@ class ServerCallThread(threading.Thread):
raw_data_s = raw_data.decode()
response_data = json.loads(raw_data_s)
else:
raise Exception(f'invalid responsetype: {self._response_type}')
except (urllib.error.URLError, ConnectionError):
# Server rejected us, broken pipe, etc. It happens. Ignoring.
response_data = None
raise TypeError(f'invalid responsetype: {self._response_type}')
except Exception as exc:
# Any other error here is unexpected, so let's make a note of it.
print('Exc in ServerCallThread:', exc)
import traceback
traceback.print_exc()
import errno
do_print = False
response_data = None
# Ignore common network errors; note unexpected ones.
if isinstance(
exc,
(urllib.error.URLError, ConnectionError,
http.client.IncompleteRead, http.client.BadStatusLine)):
pass
elif isinstance(exc, OSError):
if exc.errno == 10051: # Windows unreachable network error.
pass
elif exc.errno in [
errno.ETIMEDOUT, errno.EHOSTUNREACH, errno.ENETUNREACH
]:
pass
else:
do_print = True
elif (self._response_type == ServerResponseType.JSON
and isinstance(exc, json.decoder.JSONDecodeError)):
pass
else:
do_print = True
if do_print:
# Any other error here is unexpected,
# so let's make a note of it,
print(f'Error in ServerCallThread'
f' (response-type={self._response_type},'
f' response-data={response_data}):')
import traceback
traceback.print_exc()
if self._callback is not None:
_ba.pushcall(_general.Call(self._run_callback, response_data),
from_other_thread=True)
def serverget(
def master_server_get(
request: str,
data: Dict[str, Any],
callback: Optional[ServerCallbackType] = None,
@ -170,7 +177,7 @@ def serverget(
ServerCallThread(request, 'get', data, callback, response_type).start()
def serverput(
def master_server_post(
request: str,
data: Dict[str, Any],
callback: Optional[ServerCallbackType] = None,

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Defines NodeActor class."""
from __future__ import annotations

View File

@ -0,0 +1,330 @@
# Released under the MIT License. See LICENSE for details.
#
"""Player related functionality."""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, TypeVar, Generic, cast
import _ba
from ba._error import (SessionPlayerNotFoundError, print_exception,
ActorNotFoundError)
from ba._messages import DeathType, DieMessage
if TYPE_CHECKING:
from typing import (Type, Optional, Sequence, Dict, Any, Union, Tuple,
Callable)
import ba
PlayerType = TypeVar('PlayerType', bound='ba.Player')
TeamType = TypeVar('TeamType', bound='ba.Team')
@dataclass
class PlayerInfo:
"""Holds basic info about a player.
Category: Gameplay Classes
"""
name: str
character: str
@dataclass
class StandLocation:
"""Describes a point in space and an angle to face.
Category: Gameplay Classes
"""
position: ba.Vec3
angle: Optional[float] = None
class Player(Generic[TeamType]):
"""A player in a specific ba.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.
Attributes:
actor
The ba.Actor associated with the player.
"""
# These are instance attrs but we define them at the type level so
# their type annotations are introspectable (for docs generation).
character: str
actor: Optional[ba.Actor]
color: Sequence[float]
highlight: Sequence[float]
_team: TeamType
_sessionplayer: ba.SessionPlayer
_nodeactor: Optional[ba.NodeActor]
_expired: bool
_postinited: bool
_customdata: dict
# NOTE: avoiding having any __init__() here since it seems to not
# get called by default if a dataclass inherits from us.
# This also lets us keep trivial player classes cleaner by skipping
# the super().__init__() line.
def postinit(self, sessionplayer: ba.SessionPlayer) -> None:
"""Wire up a newly created player.
(internal)
"""
from ba._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
# internal game logic. So complain loudly if we find one.
if type(self).__eq__ is not object.__eq__:
raise RuntimeError(
f'Player class {type(self)} defines an equality'
f' operator (__eq__) which will break internal'
f' logic. Please remove it.\n'
f'For dataclasses you can do "dataclass(eq=False)"'
f' in the class decorator.')
self.actor = None
self.character = ''
self._nodeactor: Optional[ba.NodeActor] = None
self._sessionplayer = sessionplayer
self.character = sessionplayer.character
self.color = sessionplayer.color
self.highlight = sessionplayer.highlight
self._team = cast(TeamType, 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})
self._nodeactor = NodeActor(node)
sessionplayer.setnode(node)
def leave(self) -> None:
"""Called when the Player leaves a running game.
(internal)
"""
assert self._postinited
assert not self._expired
try:
# If they still have an actor, kill it.
if self.actor:
self.actor.handlemessage(DieMessage(how=DeathType.LEFT_GAME))
self.actor = None
except Exception:
print_exception(f'Error killing actor on leave for {self}')
self._nodeactor = None
del self._team
del self._customdata
def expire(self) -> None:
"""Called when the Player is expiring (when its Activity does so).
(internal)
"""
assert self._postinited
assert not self._expired
self._expired = True
try:
self.on_expire()
except Exception:
print_exception(f'Error in on_expire for {self}.')
self._nodeactor = None
self.actor = None
del self._team
del self._customdata
def on_expire(self) -> None:
"""Can be overridden to handle player expiration.
The player expires when the Activity it is a part of expires.
Expired players should no longer run any game logic (which will
likely error). They should, however, remove any references to
players/teams/games/etc. which could prevent them from being freed.
"""
@property
def team(self) -> TeamType:
"""The ba.Team for this player."""
assert self._postinited
assert not self._expired
return self._team
@property
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
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
continue to be referenced after it is no longer part of the game.
"""
assert self._postinited
assert not self._expired
return self._customdata
@property
def sessionplayer(self) -> ba.SessionPlayer:
"""Return the ba.SessionPlayer corresponding to this Player.
Throws a ba.SessionPlayerNotFoundError if it does not exist.
"""
assert self._postinited
if bool(self._sessionplayer):
return self._sessionplayer
raise SessionPlayerNotFoundError()
@property
def node(self) -> ba.Node:
"""A ba.Node of type 'player' associated with this Player.
This node can be used to get a generic player position/etc.
"""
assert self._postinited
assert not self._expired
assert self._nodeactor
return self._nodeactor.node
@property
def position(self) -> ba.Vec3:
"""The position of the player, as defined by its current ba.Actor.
If the player currently has no actor, raises a ba.ActorNotFoundError.
"""
assert self._postinited
assert not self._expired
if self.actor is None:
raise ActorNotFoundError
return _ba.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.
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
the right thing both for Player objects and values of None.
"""
assert self._postinited
return self._sessionplayer.exists() and not self._expired
def getname(self, full: bool = False, icon: bool = True) -> str:
"""getname(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.
"""
assert self._postinited
assert not self._expired
return self._sessionplayer.getname(full=full, icon=icon)
def is_alive(self) -> bool:
"""is_alive() -> bool
Returns True if the player has a ba.Actor assigned and its
is_alive() method return True. False is returned otherwise.
"""
assert self._postinited
assert not self._expired
return self.actor is not None and self.actor.is_alive()
def get_icon(self) -> Dict[str, Any]:
"""get_icon() -> Dict[str, Any]
Returns the character's icon (images, colors, etc contained in a dict)
"""
assert self._postinited
assert not self._expired
return self._sessionplayer.get_icon()
def assigninput(self, inputtype: Union[ba.InputType, Tuple[ba.InputType,
...]],
call: Callable) -> None:
"""assigninput(type: Union[ba.InputType, Tuple[ba.InputType, ...]],
call: Callable) -> None
Set the python callable to be run for one or more types of input.
"""
assert self._postinited
assert not self._expired
return self._sessionplayer.assigninput(type=inputtype, call=call)
def resetinput(self) -> None:
"""resetinput() -> None
Clears out the player's assigned input actions.
"""
assert self._postinited
assert not self._expired
self._sessionplayer.resetinput()
def __bool__(self) -> bool:
return self.exists()
class EmptyPlayer(Player['ba.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.
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
for both.
"""
# NOTE: It seems we might not need these playercast() calls; have gone
# the direction where things returning players generally take a type arg
# and do this themselves; that way the user is 'forced' to deal with types
# 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.
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
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.
"""
assert isinstance(player, totype)
return player
# NOTE: ideally we should have a single playercast() call and use overloads
# 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: Optional[ba.Player]) -> Optional[PlayerType]:
"""A variant of ba.playercast() for use with optional ba.Player values.
Category: Gameplay Functions
"""
assert isinstance(player, (totype, type(None)))
return player

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Playlist related functionality."""
from __future__ import annotations
@ -46,7 +28,7 @@ def filter_playlist(playlist: PlaylistType,
# pylint: disable=too-many-locals
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
from ba import _meta
import _ba
from ba import _map
from ba import _general
from ba import _gameactivity
@ -54,7 +36,7 @@ def filter_playlist(playlist: PlaylistType,
unowned_maps: Sequence[str]
if remove_unowned or mark_unowned:
unowned_maps = _map.get_unowned_maps()
unowned_game_types = _meta.get_unowned_game_types()
unowned_game_types = _ba.app.meta.get_unowned_game_types()
else:
unowned_maps = []
unowned_game_types = set()
@ -80,7 +62,7 @@ def filter_playlist(playlist: PlaylistType,
# the actual game class. add successful ones to our initial list
# to present to the user.
if not isinstance(entry['type'], str):
raise Exception('invalid entry format')
raise TypeError('invalid entry format')
try:
# Do some type filters for backwards compat.
if entry['type'] in ('Assault.AssaultGame',
@ -151,11 +133,10 @@ def filter_playlist(playlist: PlaylistType,
entry['is_unowned_game'] = True
# Make sure all settings the game defines are present.
neededsettings = gameclass.get_settings(sessiontype)
for setting_name, setting in neededsettings:
if (setting_name not in entry['settings']
and 'default' in setting):
entry['settings'][setting_name] = setting['default']
neededsettings = gameclass.get_available_settings(sessiontype)
for setting in neededsettings:
if setting.name not in entry['settings']:
entry['settings'][setting.name] = setting.default
goodlist.append(entry)
except ImportError as exc:
print(f'Import failed while scanning playlist: {exc}')

View File

@ -0,0 +1,96 @@
# Released under the MIT License. See LICENSE for details.
#
"""Plugin related functionality."""
from __future__ import annotations
from typing import TYPE_CHECKING
from dataclasses import dataclass
import _ba
if TYPE_CHECKING:
from typing import List, Dict
import ba
class PluginSubsystem:
"""Subsystem for plugin handling in the app.
Category: App Classes
Access the single shared instance of this class at 'ba.app.plugins'.
"""
def __init__(self) -> None:
self.potential_plugins: List[ba.PotentialPlugin] = []
self.active_plugins: Dict[str, ba.Plugin] = {}
def on_app_launch(self) -> None:
"""Should be called at app launch time."""
# Load up our plugins and go ahead and call their on_app_launch calls.
self.load_plugins()
for plugin in self.active_plugins.values():
try:
plugin.on_app_launch()
except Exception:
from ba import _error
_error.print_exception('Error in plugin on_app_launch()')
def load_plugins(self) -> None:
"""(internal)"""
from ba._general import getclass
# Note: the plugins we load is purely based on what's enabled
# in the app config. Our meta-scan gives us a list of available
# plugins, but that is only used to give the user a list of plugins
# that they can enable. (we wouldn't want to look at meta-scan here
# anyway because it may not be done yet at this point in the launch)
plugstates: Dict[str, Dict] = _ba.app.config.get('Plugins', {})
assert isinstance(plugstates, dict)
plugkeys: List[str] = sorted(key for key, val in plugstates.items()
if val.get('enabled', False))
for plugkey in plugkeys:
try:
cls = getclass(plugkey, Plugin)
except Exception as exc:
_ba.log(f"Error loading plugin class '{plugkey}': {exc}",
to_server=False)
continue
try:
plugin = cls()
assert plugkey not in self.active_plugins
self.active_plugins[plugkey] = plugin
except Exception:
from ba import _error
_error.print_exception(f'Error loading plugin: {plugkey}')
@dataclass
class PotentialPlugin:
"""Represents a ba.Plugin which can potentially be loaded.
Category: App Classes
These generally represent plugins which were detected by the
meta-tag scan. However they may also represent plugins which
were previously set to be loaded but which were unable to be
for some reason. In that case, 'available' will be set to False.
"""
display_name: ba.Lstr
class_path: str
available: bool
class Plugin:
"""A plugin to alter app behavior in some way.
Category: App Classes
Plugins are discoverable by the meta-tag system
and the user can select which ones they want to activate.
Active plugins are then called at specific times as the
app is running in order to modify its behavior in some way.
"""
def on_app_launch(self) -> None:
"""Called when the app is being launched."""

View File

@ -1,24 +1,7 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Powerup related functionality."""
from __future__ import annotations
from typing import TYPE_CHECKING
@ -31,7 +14,6 @@ if TYPE_CHECKING:
@dataclass
class PowerupMessage:
# noinspection PyUnresolvedReferences
"""A message telling an object to accept a powerup.
Category: Message Classes
@ -44,14 +26,14 @@ class PowerupMessage:
The type of powerup to be granted (a string).
See ba.Powerup.poweruptype for available type values.
source_node
sourcenode
The node the powerup game from, or None otherwise.
If a powerup is accepted, a ba.PowerupAcceptMessage should be sent
back to the source_node to inform it of the fact. This will generally
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.
"""
poweruptype: str
source_node: Optional[ba.Node] = None
sourcenode: Optional[ba.Node] = None
@dataclass

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality related to player profiles."""
from __future__ import annotations
@ -35,7 +17,7 @@ PLAYER_COLORS = [(1, 0.15, 0.15), (0.2, 1, 0.2), (0.1, 0.1, 1), (0.2, 1, 1),
(0.5, 0.25, 1.0), (1, 1, 0), (1, 0.5, 0), (1, 0.3, 0.5),
(0.1, 0.1, 0.5), (0.4, 0.2, 0.1), (0.1, 0.35, 0.1),
(1, 0.8, 0.5), (0.4, 0.05, 0.05), (0.13, 0.13, 0.13),
(0.5, 0.5, 0.5), (1, 1, 1)] # yapf: disable
(0.5, 0.5, 0.5), (1, 1, 1)]
def get_player_colors() -> List[Tuple[float, float, float]]:
@ -50,16 +32,16 @@ def get_player_profile_icon(profilename: str) -> str:
"""
from ba._enums import SpecialChar
bs_config = _ba.app.config
appconfig = _ba.app.config
icon: str
try:
is_global = bs_config['Player Profiles'][profilename]['global']
except Exception:
is_global = appconfig['Player Profiles'][profilename]['global']
except KeyError:
is_global = False
if is_global:
try:
icon = bs_config['Player Profiles'][profilename]['icon']
except Exception:
icon = appconfig['Player Profiles'][profilename]['icon']
except KeyError:
icon = _ba.charstr(SpecialChar.LOGO)
else:
icon = ''
@ -71,36 +53,36 @@ def get_player_profile_colors(
profiles: Dict[str, Dict[str, Any]] = None
) -> Tuple[Tuple[float, float, float], Tuple[float, float, float]]:
"""Given a profile, return colors for them."""
bs_config = _ba.app.config
appconfig = _ba.app.config
if profiles is None:
profiles = bs_config['Player Profiles']
profiles = appconfig['Player Profiles']
# special case - when being asked for a random color in kiosk mode,
# always return default purple
if _ba.app.kiosk_mode and profilename is None:
# 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:
color = (0.5, 0.4, 1.0)
highlight = (0.4, 0.4, 0.5)
else:
try:
assert profilename is not None
color = profiles[profilename]['color']
except Exception:
# key off name if possible
except (KeyError, AssertionError):
# Key off name if possible.
if profilename is None:
# first 6 are bright-ish
# First 6 are bright-ish.
color = PLAYER_COLORS[random.randrange(6)]
else:
# first 6 are bright-ish
# First 6 are bright-ish.
color = PLAYER_COLORS[sum([ord(c) for c in profilename]) % 6]
try:
assert profilename is not None
highlight = profiles[profilename]['highlight']
except Exception:
# key off name if possible
except (KeyError, AssertionError):
# Key off name if possible.
if profilename is None:
# last 2 are grey and white; ignore those or we
# get lots of old-looking players
# Last 2 are grey and white; ignore those or we
# get lots of old-looking players.
highlight = PLAYER_COLORS[random.randrange(
len(PLAYER_COLORS) - 2)]
else:

View File

@ -0,0 +1,56 @@
# Released under the MIT License. See LICENSE for details.
#
"""Score related functionality."""
from __future__ import annotations
from enum import Enum, unique
from dataclasses import dataclass
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import ba
@unique
class ScoreType(Enum):
"""Type of scores.
Category: Enums
"""
SECONDS = 's'
MILLISECONDS = 'ms'
POINTS = 'p'
@dataclass
class ScoreConfig:
"""Settings for how a game handles scores.
Category: Gameplay Classes
Attributes:
label
A label show to the user for scores; 'Score', 'Time Survived', etc.
scoretype
How the score value should be displayed.
lower_is_better
Whether lower scores are preferable. Higher scores are by default.
none_is_winner
Whether a value of None is considered better than other scores.
By default it is not.
version
To change high-score lists used by a game without renaming the game,
change this. Defaults to an empty string.
"""
label: str = 'Score'
scoretype: ba.ScoreType = ScoreType.POINTS
lower_is_better: bool = False
none_is_winner: bool = False
version: str = ''

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality related to running the game in server-mode."""
from __future__ import annotations
@ -26,14 +8,14 @@ import time
from typing import TYPE_CHECKING
from efro.terminal import Clr
from ba._enums import TimeType
from ba._freeforallsession import FreeForAllSession
from ba._dualteamsession import DualTeamSession
from bacommon.servermanager import (ServerCommand, StartServerModeCommand,
ShutdownCommand, ShutdownReason,
ChatMessageCommand, ScreenMessageCommand,
ClientListCommand, KickCommand)
import _ba
from ba._enums import TimeType
from ba._freeforallsession import FreeForAllSession
from ba._dualteamsession import DualTeamSession
if TYPE_CHECKING:
from typing import Optional, Dict, Any, Type
@ -65,6 +47,7 @@ def _cmd(command_data: bytes) -> None:
if isinstance(command, ScreenMessageCommand):
assert _ba.app.server is not None
# Note: we have to do transient messages if
# clients is specified, so they won't show up
# in replays.
@ -137,7 +120,7 @@ class ServerController:
for client in roster:
if client['client_id'] == -1:
continue
spec = json.loads(client['specString'])
spec = json.loads(client['spec_string'])
name = spec['n']
players = ', '.join(n['name'] for n in client['players'])
clientid = client['client_id']
@ -183,7 +166,7 @@ class ServerController:
return False
def _execute_shutdown(self) -> None:
from ba._lang import Lstr
from ba._language import Lstr
if self._executing_shutdown:
return
self._executing_shutdown = True
@ -192,19 +175,19 @@ class ServerController:
_ba.screenmessage(Lstr(resource='internal.serverRestartingText'),
color=(1, 0.5, 0.0))
print(f'{Clr.SBLU}Exiting for server-restart'
f' at {timestrval}{Clr.RST}')
f' at {timestrval}.{Clr.RST}')
else:
_ba.screenmessage(Lstr(resource='internal.serverShuttingDownText'),
color=(1, 0.5, 0.0))
print(f'{Clr.SBLU}Exiting for server-shutdown'
f' at {timestrval}{Clr.RST}')
f' at {timestrval}.{Clr.RST}')
with _ba.Context('ui'):
_ba.timer(2.0, _ba.quit, timetype=TimeType.REAL)
def _run_access_check(self) -> None:
"""Check with the master server to see if we're likely joinable."""
from ba._netutils import serverget
serverget(
from ba._netutils import master_server_get
master_server_get(
'bsAccessCheck',
{
'port': _ba.get_game_port(),
@ -241,6 +224,7 @@ class ServerController:
f' joinable from the internet.{poststr}{Clr.RST}')
def _prepare_to_serve(self) -> None:
"""Run in a timer to do prep before beginning to serve."""
signed_in = _ba.get_account_state() == 'signed_in'
if not signed_in:
@ -319,9 +303,11 @@ class ServerController:
if self._first_run:
curtimestr = time.strftime('%c')
print(f'{Clr.BLD}{Clr.BLU}BallisticaCore {app.version}'
f' ({app.build_number})'
f' entering server-mode {curtimestr}{Clr.RST}')
_ba.log(
f'{Clr.BLD}{Clr.BLU}{_ba.appnameupper()} {app.version}'
f' ({app.build_number})'
f' entering server-mode {curtimestr}{Clr.RST}',
to_server=False)
if sessiontype is FreeForAllSession:
appcfg['Free-for-All Playlist Selection'] = self._playlist_name
@ -339,15 +325,28 @@ class ServerController:
_ba.set_authenticate_clients(self._config.authenticate_clients)
_ba.set_enable_default_kick_voting(
self._config.enable_default_kick_voting)
_ba.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_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)
# And here we go.
_ba.new_host_session(sessiontype)
# 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(playlist_type='Random',
playlist_name='__default__',
player_count=self._config.stress_test_players,
round_duration=30)
else:
_ba.new_host_session(sessiontype)
if not self._ran_access_check:
# 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:
self._run_access_check()
self._ran_access_check = True

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,84 @@
# Released under the MIT License. See LICENSE for details.
#
"""Functionality for user-controllable settings."""
from __future__ import annotations
from typing import TYPE_CHECKING
from dataclasses import dataclass
if TYPE_CHECKING:
from typing import Any, List, Tuple
@dataclass
class Setting:
"""Defines a user-controllable setting for a game or other entity.
Category: Gameplay Classes
"""
name: str
default: Any
@dataclass
class BoolSetting(Setting):
"""A boolean game setting.
Category: Settings Classes
"""
default: bool
@dataclass
class IntSetting(Setting):
"""An integer game setting.
Category: Settings Classes
"""
default: int
min_value: int = 0
max_value: int = 9999
increment: int = 1
@dataclass
class FloatSetting(Setting):
"""A floating point game setting.
Category: Settings Classes
"""
default: float
min_value: float = 0.0
max_value: float = 9999.0
increment: float = 1.0
@dataclass
class ChoiceSetting(Setting):
"""A setting with multiple choices.
Category: Settings Classes
"""
choices: List[Tuple[str, Any]]
@dataclass
class IntChoiceSetting(ChoiceSetting):
"""An int setting with multiple choices.
Category: Settings Classes
"""
default: int
choices: List[Tuple[str, int]]
@dataclass
class FloatChoiceSetting(ChoiceSetting):
"""A float setting with multiple choices.
Category: Settings Classes
"""
default: float
choices: List[Tuple[str, float]]

View File

@ -1,25 +1,6 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality related to scores and statistics."""
from __future__ import annotations
import random
@ -28,6 +9,8 @@ from typing import TYPE_CHECKING
from dataclasses import dataclass
import _ba
from ba._error import (print_exception, print_error, SessionTeamNotFoundError,
SessionPlayerNotFoundError, NotFoundError)
if TYPE_CHECKING:
import ba
@ -37,7 +20,6 @@ if TYPE_CHECKING:
@dataclass
class PlayerScoredMessage:
# noinspection PyUnresolvedReferences
"""Informs something that a ba.Player scored.
Category: Message Classes
@ -61,8 +43,8 @@ class PlayerRecord:
"""
character: str
def __init__(self, name: str, name_full: str, player: ba.Player,
stats: ba.Stats):
def __init__(self, name: str, name_full: str,
sessionplayer: ba.SessionPlayer, stats: ba.Stats):
self.name = name
self.name_full = name_full
self.score = 0
@ -74,43 +56,43 @@ class PlayerRecord:
self._multi_kill_timer: Optional[ba.Timer] = None
self._multi_kill_count = 0
self._stats = weakref.ref(stats)
self._last_player: Optional[ba.Player] = None
self._player: Optional[ba.Player] = None
self._team: Optional[ReferenceType[ba.Team]] = None
self._last_sessionplayer: Optional[ba.SessionPlayer] = None
self._sessionplayer: Optional[ba.SessionPlayer] = None
self._sessionteam: Optional[ReferenceType[ba.SessionTeam]] = None
self.streak = 0
self.associate_with_player(player)
self.associate_with_sessionplayer(sessionplayer)
@property
def team(self) -> ba.Team:
"""The ba.Team the last associated player was last on.
def team(self) -> ba.SessionTeam:
"""The ba.SessionTeam the last associated player was last on.
This can still return a valid result even if the player is gone.
Raises a ba.TeamNotFoundError if the team no longer exists.
Raises a ba.SessionTeamNotFoundError if the team no longer exists.
"""
assert self._team is not None
team = self._team()
assert self._sessionteam is not None
team = self._sessionteam()
if team is None:
from ba._error import TeamNotFoundError
raise TeamNotFoundError()
raise SessionTeamNotFoundError()
return team
@property
def player(self) -> ba.Player:
"""Return the instance's associated ba.Player.
def player(self) -> ba.SessionPlayer:
"""Return the instance's associated ba.SessionPlayer.
Raises a ba.PlayerNotFoundError if the player no longer exists."""
if not self._player:
from ba._error import PlayerNotFoundError
raise PlayerNotFoundError()
return self._player
Raises a ba.SessionPlayerNotFoundError if the player
no longer exists.
"""
if not self._sessionplayer:
raise SessionPlayerNotFoundError()
return self._sessionplayer
def get_name(self, full: bool = False) -> str:
def getname(self, full: bool = False) -> str:
"""Return the player entry's name."""
return self.name_full if full else self.name
def get_icon(self) -> Dict[str, Any]:
"""Get the icon for this instance's player."""
player = self._last_player
player = self._last_sessionplayer
assert player is not None
return player.get_icon()
@ -127,28 +109,29 @@ class PlayerRecord:
return stats.getactivity()
return None
def associate_with_player(self, player: ba.Player) -> None:
"""Associate this entry with a ba.Player."""
self._team = weakref.ref(player.team)
self.character = player.character
self._last_player = player
self._player = player
def associate_with_sessionplayer(self,
sessionplayer: ba.SessionPlayer) -> None:
"""Associate this entry with a ba.SessionPlayer."""
self._sessionteam = weakref.ref(sessionplayer.sessionteam)
self.character = sessionplayer.character
self._last_sessionplayer = sessionplayer
self._sessionplayer = sessionplayer
self.streak = 0
def _end_multi_kill(self) -> None:
self._multi_kill_timer = None
self._multi_kill_count = 0
def get_last_player(self) -> ba.Player:
def get_last_sessionplayer(self) -> ba.SessionPlayer:
"""Return the last ba.Player we were associated with."""
assert self._last_player is not None
return self._last_player
assert self._last_sessionplayer is not None
return self._last_sessionplayer
def submit_kill(self, showpoints: bool = True) -> None:
"""Submit a kill for this player entry."""
# FIXME Clean this up.
# pylint: disable=too-many-statements
from ba._lang import Lstr
from ba._language import Lstr
from ba._general import Call
self._multi_kill_count += 1
stats = self._stats()
@ -203,16 +186,21 @@ class PlayerRecord:
from bastd.actor.popuptext import PopupText
# Only award this if they're still alive and we can get
# their pos.
if self._player is not None and self._player.node:
our_pos = self._player.node.position
else:
# a current position for them.
our_pos: Optional[ba.Vec3] = None
if self._sessionplayer:
if self._sessionplayer.activityplayer is not None:
try:
our_pos = self._sessionplayer.activityplayer.position
except NotFoundError:
pass
if our_pos is None:
return
# Jitter position a bit since these often come in clusters.
our_pos = (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)
our_pos = _ba.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)
activity = self.getactivity()
if activity is not None:
PopupText(Lstr(
@ -256,16 +244,15 @@ class Stats:
self.orchestrahitsound3: Optional[ba.Sound] = None
self.orchestrahitsound4: Optional[ba.Sound] = None
def set_activity(self, activity: Optional[ba.Activity]) -> None:
def setactivity(self, activity: Optional[ba.Activity]) -> None:
"""Set the current activity for this instance."""
self._activity = None if activity is None else weakref.ref(activity)
# Load our media into this activity's context.
if activity is not None:
if activity.is_expired():
from ba import _error
_error.print_error('unexpected finalized activity')
if activity.expired:
print_error('unexpected finalized activity')
else:
with _ba.Context(activity):
self._load_activity_media()
@ -303,17 +290,16 @@ class Stats:
s_player.accum_killed_count = 0
s_player.streak = 0
def register_player(self, player: ba.Player) -> None:
"""Register a player with this score-set."""
name = player.get_name()
name_full = player.get_name(full=True)
try:
def register_sessionplayer(self, player: ba.SessionPlayer) -> None:
"""Register a ba.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:
# If the player already exists, update his character and such as
# it may have changed.
self._player_records[name].associate_with_player(player)
except Exception:
# FIXME: Shouldn't use top level Exception catch for logic.
# Should only have this as a fallback and always log it.
self._player_records[name].associate_with_sessionplayer(player)
else:
name_full = player.getname(full=True)
self._player_records[name] = PlayerRecord(name, name_full, player,
self)
@ -324,16 +310,11 @@ class Stats:
# Go through our player records and return ones whose player id still
# corresponds to a player with that name.
for record_id, record in self._player_records.items():
lastplayer = record.get_last_player()
if lastplayer and lastplayer.get_name() == record_id:
lastplayer = record.get_last_sessionplayer()
if lastplayer and lastplayer.getname() == record_id:
records[record_id] = record
return records
def player_got_hit(self, player: ba.Player) -> None:
"""Call this when a player got hit."""
s_player = self._player_records[player.get_name()]
s_player.streak = 0
def player_scored(self,
player: ba.Player,
base_points: int = 1,
@ -360,9 +341,9 @@ class Stats:
from bastd.actor.popuptext import PopupText
from ba import _math
from ba._gameactivity import GameActivity
from ba._lang import Lstr
from ba._language import Lstr
del victim_player # Currently unused.
name = player.get_name()
name = player.getname()
s_player = self._player_records[name]
if kill:
@ -382,14 +363,13 @@ class Stats:
assert self._activity is not None
activity = self._activity()
if isinstance(activity, GameActivity):
name_full = player.get_name(full=True, icon=False)
name_full = player.getname(full=True, icon=False)
activity.show_zoom_message(
Lstr(resource='nameScoresText',
subs=[('${NAME}', name_full)]),
color=_math.normalized_color(player.team.color))
except Exception:
from ba import _error
_error.print_exception('error showing big_message')
print_exception('error showing big_message')
# If we currently have a actor, pop up a score over it.
if display and showpoints:
@ -430,8 +410,7 @@ class Stats:
color=player.color,
image=player.get_icon())
except Exception:
from ba import _error
_error.print_exception('error announcing score')
print_exception('error announcing score')
s_player.score += points
s_player.accumscore += points
@ -449,8 +428,8 @@ class Stats:
killed: bool = False,
killer: ba.Player = None) -> None:
"""Should be called when a player is killed."""
from ba._lang import Lstr
name = player.get_name()
from ba._language import Lstr
name = player.getname()
prec = self._player_records[name]
prec.streak = 0
if killed:
@ -458,17 +437,17 @@ class Stats:
prec.killed_count += 1
try:
if killed and _ba.getactivity().announce_player_deaths:
if killer == player:
if killer is player:
_ba.screenmessage(Lstr(resource='nameSuicideText',
subs=[('${NAME}', name)]),
top=True,
color=player.color,
image=player.get_icon())
elif killer is not None:
if killer.team == player.team:
if killer.team is player.team:
_ba.screenmessage(Lstr(resource='nameBetrayedText',
subs=[('${NAME}',
killer.get_name()),
killer.getname()),
('${VICTIM}', name)]),
top=True,
color=killer.color,
@ -476,7 +455,7 @@ class Stats:
else:
_ba.screenmessage(Lstr(resource='nameKilledText',
subs=[('${NAME}',
killer.get_name()),
killer.getname()),
('${VICTIM}', name)]),
top=True,
color=killer.color,
@ -488,5 +467,4 @@ class Stats:
color=player.color,
image=player.get_icon())
except Exception:
from ba import _error
_error.print_exception('error announcing kill')
print_exception('error announcing kill')

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Store related functionality for classic mode."""
from __future__ import annotations
@ -39,15 +21,16 @@ def get_store_item(item: str) -> Dict[str, Any]:
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 _lang
from ba import _language
from ba import _map
item_info = get_store_item(item_name)
if item_name.startswith('characters.'):
return _lang.Lstr(translate=('characterNames', item_info['character']))
return _language.Lstr(translate=('characterNames',
item_info['character']))
if item_name in ['upgrades.pro', 'pro']:
return _lang.Lstr(resource='store.bombSquadProNameText',
subs=[('${APP_NAME}',
_lang.Lstr(resource='titleText'))])
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)
@ -55,8 +38,8 @@ def get_store_item_name_translated(item_name: str) -> ba.Lstr:
gametype: Type[ba.GameActivity] = item_info['gametype']
return gametype.get_display_string()
if item_name.startswith('icons.'):
return _lang.Lstr(resource='editProfileWindow.iconText')
raise Exception('unrecognized item: ' + item_name)
return _language.Lstr(resource='editProfileWindow.iconText')
raise ValueError('unrecognized item: ' + item_name)
def get_store_item_display_size(item_name: str) -> Tuple[float, float]:
@ -457,7 +440,6 @@ def get_available_sale_time(tab: str) -> Optional[int]:
# pylint: disable=too-many-locals
try:
import datetime
from ba._account import have_pro
from ba._enums import TimeType, TimeFormat
app = _ba.app
sale_times: List[Optional[int]] = []
@ -465,7 +447,7 @@ def get_available_sale_time(tab: str) -> Optional[int]:
# Calc time for our pro sale (old special case).
if tab == 'extras':
config = app.config
if have_pro():
if app.accounts.have_pro():
return None
# If we haven't calced/loaded start times yet.
@ -494,7 +476,7 @@ def get_available_sale_time(tab: str) -> Optional[int]:
assert app.pro_sale_start_val is not None
val: Optional[int] = max(
0, app.pro_sale_start_val -
(int(_ba.time(TimeType.REAL, TimeFormat.MILLISECONDS)) -
(_ba.time(TimeType.REAL, TimeFormat.MILLISECONDS) -
app.pro_sale_start_time))
# Keep the value in the config up to date. I suppose we should
@ -518,8 +500,9 @@ def get_available_sale_time(tab: str) -> Optional[int]:
if to_end > 0:
sale_times.append(int(to_end * 1000))
# Return the smallest time i guess?
return min(sale_times) if sale_times else None
# 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

View File

@ -1,117 +1,212 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Defines Team class."""
"""Team related functionality."""
from __future__ import annotations
from typing import TYPE_CHECKING
import weakref
from typing import TYPE_CHECKING, TypeVar, Generic
from ba._error import print_exception
if TYPE_CHECKING:
from typing import Dict, List, Sequence, Any, Tuple, Union
from weakref import ReferenceType
from typing import Dict, List, Sequence, Tuple, Union, Optional
import ba
class Team:
"""A team of one or more ba.Players.
class SessionTeam:
"""A team of one or more ba.SessionPlayers.
Category: Gameplay Classes
Note that a player *always* has a team;
Note that a SessionPlayer *always* has a SessionTeam;
in some cases, such as free-for-all ba.Sessions,
each team consists of just one ba.Player.
each SessionTeam consists of just one SessionPlayer.
Attributes:
name
The team's name.
id
The unique numeric id of the team.
color
The team's color.
players
The list of ba.Players on the team.
The list of ba.SessionPlayers on the team.
gamedata
A dict for use by the current ba.Activity
for storing data associated with this team.
This gets cleared for each new ba.Activity.
sessiondata
customdata
A dict for use by the current ba.Session for
storing data associated with this team.
Unlike gamedata, this persists for the duration
Unlike customdata, this persists for the duration
of the session.
"""
# Annotate our attr types at the class level so they're introspectable.
name: Union[ba.Lstr, str]
color: Tuple[float, ...]
players: List[ba.Player]
gamedata: Dict
sessiondata: Dict
color: Tuple[float, ...] # FIXME: can't we make this fixed len?
players: List[ba.SessionPlayer]
customdata: dict
id: int
def __init__(self,
team_id: int = 0,
name: Union[ba.Lstr, str] = '',
color: Sequence[float] = (1.0, 1.0, 1.0)):
"""Instantiate a ba.Team.
"""Instantiate a ba.SessionTeam.
In most cases, all teams are provided to you by the ba.Session,
ba.Session, so calling this shouldn't be necessary.
"""
# TODO: Once we spin off team copies for each activity, we don't
# need to bother with trying to lock things down, since it won't
# matter at that point if the activity mucks with them.
# Temporarily allow us to set our own attrs
# (keeps pylint happier than using __setattr__ explicitly for all).
object.__setattr__(self, '_locked', False)
self._team_id: int = team_id
self.id = team_id
self.name = name
self.color = tuple(color)
self.players = []
self.gamedata = {}
self.sessiondata = {}
self.customdata = {}
self.activityteam: Optional[Team] = None
# Now prevent further attr sets.
self._locked = True
def get_id(self) -> int:
"""Returns the numeric team ID."""
return self._team_id
def reset(self) -> None:
def leave(self) -> None:
"""(internal)"""
self.reset_gamedata()
object.__setattr__(self, 'players', [])
self.customdata = {}
def reset_gamedata(self) -> None:
"""(internal)"""
object.__setattr__(self, 'gamedata', {})
def reset_sessiondata(self) -> None:
"""(internal)"""
object.__setattr__(self, 'sessiondata', {})
PlayerType = TypeVar('PlayerType', bound='ba.Player')
def __setattr__(self, name: str, value: Any) -> None:
if self._locked:
raise Exception("can't set attrs on ba.Team objects")
object.__setattr__(self, name, value)
class Team(Generic[PlayerType]):
"""A team in a specific ba.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.
"""
# Defining these types at the class level instead of in __init__ so
# that types are introspectable (these are still instance attrs).
players: List[PlayerType]
id: int
name: Union[ba.Lstr, str]
color: Tuple[float, ...] # FIXME: can't we make this fixed length?
_sessionteam: ReferenceType[SessionTeam]
_expired: bool
_postinited: bool
_customdata: dict
# NOTE: avoiding having any __init__() here since it seems to not
# get called by default if a dataclass inherits from us.
def postinit(self, sessionteam: SessionTeam) -> None:
"""Wire up a newly created SessionTeam.
(internal)
"""
# Sanity check; if a dataclass is created that inherits from us,
# it will define an equality operator by default which will break
# internal game logic. So complain loudly if we find one.
if type(self).__eq__ is not object.__eq__:
raise RuntimeError(
f'Team class {type(self)} defines an equality'
f' operator (__eq__) which will break internal'
f' logic. Please remove it.\n'
f'For dataclasses you can do "dataclass(eq=False)"'
f' in the class decorator.')
self.players = []
self._sessionteam = weakref.ref(sessionteam)
self.id = sessionteam.id
self.name = sessionteam.name
self.color = sessionteam.color
self._customdata = {}
self._expired = False
self._postinited = True
def manual_init(self, team_id: int, name: Union[ba.Lstr, str],
color: Tuple[float, ...]) -> None:
"""Manually init a team for uses such as bots."""
self.id = team_id
self.name = name
self.color = color
self._customdata = {}
self._expired = False
self._postinited = True
@property
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
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
continue to be referenced after it is no longer part of the game.
"""
assert self._postinited
assert not self._expired
return self._customdata
def leave(self) -> None:
"""Called when the Team leaves a running game.
(internal)
"""
assert self._postinited
assert not self._expired
del self._customdata
del self.players
def expire(self) -> None:
"""Called when the Team is expiring (due to the Activity expiring).
(internal)
"""
assert self._postinited
assert not self._expired
self._expired = True
try:
self.on_expire()
except Exception:
print_exception(f'Error in on_expire for {self}.')
del self._customdata
del self.players
def on_expire(self) -> None:
"""Can be overridden to handle team expiration."""
@property
def sessionteam(self) -> SessionTeam:
"""Return the ba.SessionTeam corresponding to this Team.
Throws a ba.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
raise _error.SessionTeamNotFoundError()
class EmptyTeam(Team['ba.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.
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
for both.
"""

View File

@ -1,33 +1,15 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality related to team games."""
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, TypeVar
import _ba
from ba._freeforallsession import FreeForAllSession
from ba._gameactivity import GameActivity
from ba._gameresults import TeamGameResults
from ba._gameresults import GameResults
from ba._dualteamsession import DualTeamSession
if TYPE_CHECKING:
@ -35,8 +17,11 @@ if TYPE_CHECKING:
from bastd.actor.playerspaz import PlayerSpaz
import ba
PlayerType = TypeVar('PlayerType', bound='ba.Player')
TeamType = TypeVar('TeamType', bound='ba.Team')
class TeamGameActivity(GameActivity):
class TeamGameActivity(GameActivity[PlayerType, TeamType]):
"""Base class for teams and free-for-all mode games.
Category: Gameplay Classes
@ -55,14 +40,14 @@ class TeamGameActivity(GameActivity):
return (issubclass(sessiontype, DualTeamSession)
or issubclass(sessiontype, FreeForAllSession))
def __init__(self, settings: Dict[str, Any]):
def __init__(self, settings: dict):
super().__init__(settings)
# By default we don't show kill-points in free-for-all.
# By default we don't show kill-points in free-for-all sessions.
# (there's usually some activity-specific score and we don't
# wanna confuse things)
if isinstance(_ba.getsession(), FreeForAllSession):
self._show_kill_points = False
if isinstance(self.session, FreeForAllSession):
self.show_kill_points = False
def on_transition_in(self) -> None:
# pylint: disable=cyclic-import
@ -74,8 +59,8 @@ class TeamGameActivity(GameActivity):
# (unless we're being run in co-op mode, in which case we leave
# it up to them)
if not isinstance(self.session, CoopSession):
# FIXME: Need an elegant way to store on session.
if not self.session.have_shown_controls_help_overlay:
attrname = '_have_shown_ctrl_help_overlay'
if not getattr(self.session, attrname, False):
delay = 4.0
lifespan = 10.0
if self.slow_motion:
@ -85,7 +70,7 @@ class TeamGameActivity(GameActivity):
scale=0.8,
position=(380, 200),
bright=True).autoretain()
self.session.have_shown_controls_help_overlay = True
setattr(self.session, attrname, True)
def on_begin(self) -> None:
super().on_begin()
@ -93,18 +78,17 @@ class TeamGameActivity(GameActivity):
# Award a few achievements.
if isinstance(self.session, FreeForAllSession):
if len(self.players) >= 2:
from ba import _achievement
_achievement.award_local_achievement('Free Loader')
_ba.app.ach.award_local_achievement('Free Loader')
elif isinstance(self.session, DualTeamSession):
if len(self.players) >= 4:
from ba import _achievement
_achievement.award_local_achievement('Team Player')
_ba.app.ach.award_local_achievement('Team Player')
except Exception:
from ba import _error
_error.print_exception()
def spawn_player_spaz(self,
player: ba.Player,
player: PlayerType,
position: Sequence[float] = None,
angle: float = None) -> PlayerSpaz:
"""
@ -117,13 +101,14 @@ class TeamGameActivity(GameActivity):
if position is None:
# In teams-mode get our team-start-location.
if isinstance(self.session, DualTeamSession):
position = (self.map.get_start_position(player.team.get_id()))
position = (self.map.get_start_position(player.team.id))
else:
# Otherwise do free-for-all spawn locations.
position = self.map.get_ffa_start_position(self.players)
return super().spawn_player_spaz(player, position, angle)
# FIXME: need to unify these arguments with GameActivity.end()
def end( # type: ignore
self,
results: Any = None,
@ -146,8 +131,9 @@ class TeamGameActivity(GameActivity):
if not isinstance(session, CoopSession):
do_announce = not self.has_ended()
super().end(results, delay=2.0 + announce_delay, force=force)
# Need to do this *after* end end call so that results is valid.
assert isinstance(results, TeamGameResults)
assert isinstance(results, GameResults)
if do_announce and isinstance(session, MultiTeamSession):
session.announce_game_results(
self,

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality related to game tips.
These can be shown at opportune times such as between rounds."""

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality related to tournament play."""
from __future__ import annotations

View File

@ -0,0 +1,167 @@
# Released under the MIT License. See LICENSE for details.
#
"""User interface related functionality."""
from __future__ import annotations
from typing import TYPE_CHECKING
import _ba
from ba._enums import UIScale
if TYPE_CHECKING:
from typing import Optional, Dict, Any, Callable, List, Type
from ba.ui import UICleanupCheck
import ba
class UISubsystem:
"""Consolidated UI functionality for the app.
Category: App Classes
To use this class, access the single instance of it at 'ba.app.ui'.
"""
def __init__(self) -> None:
env = _ba.env()
self.controller: Optional[ba.UIController] = None
self._main_menu_window: Optional[ba.Widget] = None
self._main_menu_location: Optional[str] = None
self._uiscale: ba.UIScale
interfacetype = env['ui_scale']
if interfacetype == 'large':
self._uiscale = UIScale.LARGE
elif interfacetype == 'medium':
self._uiscale = UIScale.MEDIUM
elif interfacetype == 'small':
self._uiscale = UIScale.SMALL
else:
raise RuntimeError(f'Invalid UIScale value: {interfacetype}')
self.window_states: Dict[Type, Any] = {} # FIXME: Kill this.
self.main_menu_selection: Optional[str] = None # FIXME: Kill this.
self.have_party_queue_window = False
self.quit_window: Any = None
self.dismiss_wii_remotes_window_call: (Optional[Callable[[],
Any]]) = None
self.cleanupchecks: List[UICleanupCheck] = []
self.upkeeptimer: Optional[ba.Timer] = 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)
self.heading_color = (0.72, 0.7, 0.75)
self.infotextcolor = (0.7, 0.9, 0.7)
# Switch our overall game selection UI flow between Play and
# Private-party playlist selection modes; should do this in
# a more elegant way once we revamp high level UI stuff a bit.
self.selecting_private_party_playlist: bool = False
@property
def uiscale(self) -> ba.UIScale:
"""Current ui scale for the app."""
return self._uiscale
def on_app_launch(self) -> None:
"""Should be run on app launch."""
from ba.ui import UIController, ui_upkeep
from ba._enums import TimeType
# IMPORTANT: If tweaking UI stuff, make sure it behaves for small,
# medium, and large UI modes. (doesn't run off screen, etc).
# The overrides below can be used to test with different sizes.
# Generally small is used on phones, medium is used on tablets/tvs,
# and large is on desktop computers or perhaps large tablets. When
# possible, run in windowed mode and resize the window to assure
# this holds true at all aspect ratios.
# UPDATE: A better way to test this is now by setting the environment
# variable BA_UI_SCALE to "small", "medium", or "large".
# This will affect system UIs not covered by the values below such
# as screen-messages. The below values remain functional, however,
# for cases such as Android where environment variables can't be set
# easily.
if bool(False): # force-test ui scale
self._uiscale = UIScale.SMALL
with _ba.Context('ui'):
_ba.pushcall(lambda: _ba.screenmessage(
f'FORCING UISCALE {self._uiscale.name} FOR TESTING',
color=(1, 0, 1),
log=True))
self.controller = UIController()
# 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)
def set_main_menu_window(self, window: ba.Widget) -> None:
"""Set the current 'main' window, replacing any existing."""
existing = self._main_menu_window
from ba._enums import TimeType
from inspect import currentframe, getframeinfo
# Let's grab the location where we were called from to report
# if we have to force-kill the existing window (which normally
# should not happen).
frameline = None
try:
frame = currentframe()
if frame is not None:
frame = frame.f_back
if frame is not None:
frameinfo = getframeinfo(frame)
frameline = f'{frameinfo.filename} {frameinfo.lineno}'
except Exception:
from ba._error import print_exception
print_exception('Error calcing line for set_main_menu_window')
# With our legacy main-menu system, the caller is responsible for
# clearing out the old main menu window when assigning the new.
# However there are corner cases where that doesn't happen and we get
# old windows stuck under the new main one. So let's guard against
# that. However, we can't simply delete the existing main window when
# a new one is assigned because the user may transition the old out
# *after* the assignment. Sigh. So, as a happy medium, let's check in
# on the old after a short bit of time and kill it if its still alive.
# That will be a bit ugly on screen but at least should un-break
# things.
def _delay_kill() -> None:
import time
if existing:
print(f'Killing old main_menu_window'
f' when called at: {frameline} t={time.time():.3f}')
existing.delete()
_ba.timer(1.0, _delay_kill, timetype=TimeType.REAL)
self._main_menu_window = window
def clear_main_menu_window(self, transition: str = None) -> None:
"""Clear any existing 'main' window with the provided transition."""
if self._main_menu_window:
if transition is not None:
_ba.containerwidget(edit=self._main_menu_window,
transition=transition)
else:
self._main_menu_window.delete()
def has_main_menu_window(self) -> bool:
"""Return whether a main menu window is present."""
return bool(self._main_menu_window)
def set_main_menu_location(self, location: str) -> None:
"""Set the location represented by the current main menu window."""
self._main_menu_location = location
def get_main_menu_location(self) -> Optional[str]:
"""Return the current named main menu location, if any."""
return self._main_menu_location

View File

@ -1,31 +1,8 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Deprecated functionality.
Classes or functions can be relocated here when they are deprecated.
Any code using them should migrate to alternative methods, as
deprecated items will eventually be fully removed.
"""
# pylint: disable=unused-import
# The Lstr class should be used for all string resources.
from ba._lang import get_resource, translate

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Exposed functionality not intended for full public use.
Classes and functions contained here, while technically 'public', may change
@ -34,27 +16,17 @@ 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._account import (on_account_state_changed,
handle_account_gained_tickets, have_pro_options,
have_pro, cache_tournament_info,
ensure_have_account_player_profile,
get_purchased_icons, get_cached_league_rank_data,
get_league_rank_points, cache_league_rank_data)
from ba._activitytypes import JoinActivity, ScoreScreenActivity
from ba._achievement import (get_achievement, set_completed_achievements,
display_achievement_banner,
get_achievements_for_coop_level)
from ba._apputils import (is_browser_likely_available, get_remote_app_name,
should_submit_debug_info, show_ad, show_ad_2)
should_submit_debug_info)
from ba._benchmark import (run_gpu_benchmark, run_cpu_benchmark,
run_media_reload_benchmark, run_stress_test)
from ba._campaign import get_campaign
from ba._campaign import getcampaign
from ba._messages import PlayerProfilesChangedMessage
from ba._meta import get_game_types
from ba._modutils import show_user_scripts
from ba._multiteamsession import DEFAULT_TEAM_COLORS, DEFAULT_TEAM_NAMES
from ba._music import do_play_music
from ba._netutils import serverget, serverput, get_ip_address_type
from ba._netutils import (master_server_get, master_server_post,
get_ip_address_type)
from ba._powerup import get_default_powerup_distribution
from ba._profile import (get_player_profile_colors, get_player_profile_icon,
get_player_colors)

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Music playback functionality using the Mac Music (formerly iTunes) app."""
from __future__ import annotations
@ -86,7 +68,7 @@ class _MacMusicAppThread(threading.Thread):
def run(self) -> None:
"""Run the Music.app thread."""
from ba._general import Call
from ba._lang import Lstr
from ba._language import Lstr
from ba._enums import TimeType
_ba.set_thread_name('BA_MacMusicAppThread')
_ba.mac_music_app_init()
@ -230,7 +212,6 @@ class _MacMusicAppThread(threading.Thread):
def _play_current_playlist(self) -> None:
try:
from ba import _lang
from ba._general import Call
assert self._current_playlist is not None
if _ba.mac_music_app_play_playlist(self._current_playlist):
@ -238,8 +219,8 @@ class _MacMusicAppThread(threading.Thread):
else:
_ba.pushcall(Call(
_ba.screenmessage,
_lang.get_resource('playlistNotFoundText') + ': \'' +
self._current_playlist + '\'', (1, 0, 0)),
_ba.app.lang.get_resource('playlistNotFoundText') +
': \'' + self._current_playlist + '\'', (1, 0, 0)),
from_other_thread=True)
except Exception:
from ba import _error

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality related to modding."""
from __future__ import annotations
@ -27,7 +9,7 @@ import os
import _ba
if TYPE_CHECKING:
from typing import Optional
from typing import Optional, List, Sequence
def get_human_readable_user_scripts_path() -> str:
@ -35,7 +17,7 @@ def get_human_readable_user_scripts_path() -> str:
This is NOT a valid filesystem path; may be something like "(SD Card)".
"""
from ba import _lang
from ba import _language
app = _ba.app
path: Optional[str] = app.python_directory_user
if path is None:
@ -50,23 +32,30 @@ def get_human_readable_user_scripts_path() -> str:
if (ext_storage_path is not None
and app.python_directory_user.startswith(ext_storage_path)):
path = ('<' +
_lang.Lstr(resource='externalStorageText').evaluate() +
_language.Lstr(resource='externalStorageText').evaluate() +
'>' + app.python_directory_user[len(ext_storage_path):])
return path
def _request_storage_permission() -> bool:
"""If needed, requests storage permission from the user (& return true)."""
from ba._language import Lstr
from ba._enums import Permission
if not _ba.have_permission(Permission.STORAGE):
_ba.playsound(_ba.getsound('error'))
_ba.screenmessage(Lstr(resource='storagePermissionAccessText'),
color=(1, 0, 0))
_ba.timer(1.0, lambda: _ba.request_permission(Permission.STORAGE))
return True
return False
def show_user_scripts() -> None:
"""Open or nicely print the location of the user-scripts directory."""
from ba import _lang
from ba._enums import Permission
app = _ba.app
# First off, if we need permission for this, ask for it.
if not _ba.have_permission(Permission.STORAGE):
_ba.playsound(_ba.getsound('error'))
_ba.screenmessage(_lang.Lstr(resource='storagePermissionAccessText'),
color=(1, 0, 0))
_ba.request_permission(Permission.STORAGE)
if _request_storage_permission():
return
# Secondly, if the dir doesn't exist, attempt to make it.
@ -107,31 +96,37 @@ def create_user_system_scripts() -> None:
(for editing and experiment with)
"""
app = _ba.app
import shutil
app = _ba.app
# First off, if we need permission for this, ask for it.
if _request_storage_permission():
return
path = (app.python_directory_user + '/sys/' + app.version)
pathtmp = path + '_tmp'
if os.path.exists(path):
shutil.rmtree(path)
if os.path.exists(path + '_tmp'):
shutil.rmtree(path + '_tmp')
os.makedirs(path + '_tmp', exist_ok=True)
if os.path.exists(pathtmp):
shutil.rmtree(pathtmp)
# Hmm; shutil.copytree doesn't seem to work nicely on android,
# so lets do it manually.
# NOTE: Should retry this now that we have 3.7 (this note was for 2.7)
src_dir = app.python_directory_ba
dst_dir = path + '_tmp'
filenames = os.listdir(app.python_directory_ba)
for fname in filenames:
print('COPYING', src_dir + '/' + fname, '->', dst_dir)
shutil.copyfile(src_dir + '/' + fname, dst_dir + '/' + fname)
def _ignore_filter(src: str, names: Sequence[str]) -> Sequence[str]:
del src, names # Unused
print('MOVING', path + '_tmp', path)
shutil.move(path + '_tmp', path)
print(
('Created system scripts at :\'' + path +
'\'\nRestart Ballistica to use them. (use ba.quit() to exit the game)'
))
# We simply skip all __pycache__ directories. (the user would have
# to blow them away anyway to make changes;
# See https://github.com/efroemling/ballistica/wiki
# /Knowledge-Nuggets#python-cache-files-gotcha
return ('__pycache__', )
print(f'COPYING "{app.python_directory_app}" -> "{pathtmp}".')
shutil.copytree(app.python_directory_app, pathtmp, ignore=_ignore_filter)
print(f'MOVING "{pathtmp}" -> "{path}".')
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)')
if app.platform == 'android':
print('Note: the new files may not be visible via '
'android-file-transfer until you restart your device.')
@ -144,9 +139,9 @@ def delete_user_system_scripts() -> None:
path = (app.python_directory_user + '/sys/' + app.version)
if os.path.exists(path):
shutil.rmtree(path)
print(
'User system scripts deleted.\nRestart Ballistica to use internal'
' scripts. (use ba.quit() to exit the game)')
print(f'User system scripts deleted.\n'
f'Restart {_ba.appname()} to use internal'
f' scripts. (use ba.quit() to exit the game)')
else:
print('User system scripts not found.')

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Music playback using OS functionality exposed through the C++ layer."""
from __future__ import annotations
@ -71,7 +53,7 @@ class OSMusicPlayer(MusicPlayer):
elif entry_type == 'musicFolder':
# Launch a thread to scan this folder and give us a random
# valid file within.
# valid file within it.
self._want_to_play = True
self._actually_playing = False
_PickFolderSongThread(name, self.get_valid_music_file_extensions(),
@ -80,9 +62,9 @@ class OSMusicPlayer(MusicPlayer):
def _on_play_folder_cb(self,
result: Union[str, List[str]],
error: Optional[str] = None) -> None:
from ba import _lang
from ba import _language
if error is not None:
rstr = (_lang.Lstr(
rstr = (_language.Lstr(
resource='internal.errorPlayingMusicText').evaluate())
if isinstance(result, str):
err_str = (rstr.replace('${MUSIC}', os.path.basename(result)) +
@ -121,8 +103,9 @@ class _PickFolderSongThread(threading.Thread):
self._path = path
def run(self) -> None:
from ba import _lang
from ba import _language
from ba._general import Call
do_print_error = True
try:
_ba.set_thread_name('BA_PickFolderSongThread')
all_files: List[str] = []
@ -134,14 +117,16 @@ class _PickFolderSongThread(threading.Thread):
all_files.insert(random.randrange(len(all_files) + 1),
root + '/' + fname)
if not all_files:
raise Exception(
_lang.Lstr(resource='internal.noMusicFilesInFolderText').
evaluate())
do_print_error = False
raise RuntimeError(
_language.Lstr(resource='internal.noMusicFilesInFolderText'
).evaluate())
_ba.pushcall(Call(self._callback, all_files, None),
from_other_thread=True)
except Exception as exc:
from ba import _error
_error.print_exception()
if do_print_error:
_error.print_exception()
try:
err_str = str(exc)
except Exception:

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Provide top level UI related functionality."""
from __future__ import annotations
@ -29,6 +11,7 @@ from typing import TYPE_CHECKING, cast, Type
import _ba
from ba._enums import TimeType
from ba._general import print_active_refs
if TYPE_CHECKING:
from typing import Optional, List, Any
@ -47,9 +30,13 @@ class Window:
Category: User Interface Classes
"""
def __init__(self, root_widget: ba.Widget):
def __init__(self, root_widget: ba.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:
"""Return the root widget."""
return self._root_widget
@ -127,11 +114,11 @@ class UIEntry:
if self._name == 'mainmenu':
from bastd.ui import mainmenu
return cast(Type[UILocation], mainmenu.MainMenuWindow)
raise Exception('unknown ui class ' + str(self._name))
raise ValueError('unknown ui class ' + str(self._name))
class UIController:
"""Wrangles UILocations.
"""Wrangles ba.UILocations.
Category: User Interface Classes
"""
@ -192,15 +179,18 @@ def uicleanupcheck(obj: Any, widget: ba.Widget) -> None:
if DEBUG_UI_CLEANUP_CHECKS:
print(f'adding uicleanup to {obj}')
if not isinstance(widget, _ba.Widget):
raise Exception('widget arg is not a ba.Widget')
raise TypeError('widget arg is not a ba.Widget')
def foobar() -> None:
"""Just testing."""
if DEBUG_UI_CLEANUP_CHECKS:
print('uicleanupcheck widget dying...')
if bool(False):
widget.add_delete_callback(foobar)
_ba.app.uicleanupchecks.append(
def foobar() -> None:
"""Just testing."""
if DEBUG_UI_CLEANUP_CHECKS:
print('uicleanupcheck widget dying...')
widget.add_delete_callback(foobar)
_ba.app.ui.cleanupchecks.append(
UICleanupCheck(obj=weakref.ref(obj),
widget=widget,
widget_death_time=None))
@ -208,10 +198,10 @@ def uicleanupcheck(obj: Any, widget: ba.Widget) -> None:
def ui_upkeep() -> None:
"""Run UI cleanup checks, etc. should be called periodically."""
app = _ba.app
ui = _ba.app.ui
remainingchecks = []
now = _ba.time(TimeType.REAL)
for check in app.uicleanupchecks:
for check in ui.cleanupchecks:
obj = check.obj()
# If the object has died, ignore and don't re-add.
@ -231,7 +221,9 @@ def ui_upkeep() -> None:
print(
'WARNING:', obj,
'is still alive 5 second after its widget died;'
' you probably have a memory leak.')
' you might have a memory leak.')
print_active_refs(obj)
else:
remainingchecks.append(check)
app.uicleanupchecks = remainingchecks
ui.cleanupchecks = remainingchecks

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""BallisticaCore standard library: games, UI, etc."""
"""Ballistica standard library: games, UI, etc."""
# ba_meta require api 6

View File

@ -1,20 +1 @@
# Copyright (c) 2011-2020 Eric Froemling
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
# Released under the MIT License. See LICENSE for details.

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality related to the co-op join screen."""
from __future__ import annotations
@ -35,17 +17,21 @@ if TYPE_CHECKING:
class CoopJoinActivity(JoinActivity):
"""Join-screen for co-op mode."""
def __init__(self, settings: Dict[str, Any]):
# We can assume our session is a CoopSession.
session: ba.CoopSession
def __init__(self, settings: dict):
super().__init__(settings)
session = ba.getsession()
session = self.session
assert isinstance(session, ba.CoopSession)
# Let's show a list of scores-to-beat for 1 player at least.
assert session.campaign is not None
level_name_full = (session.campaign.name + ':' +
session.campaign_state['level'])
config_str = (
'1p' + session.campaign.get_level(session.campaign_state['level']).
get_score_version_string().replace(' ', '_'))
session.campaign_level_name)
config_str = ('1p' + session.campaign.getlevel(
session.campaign_level_name).get_score_version_string().replace(
' ', '_'))
_ba.get_scores_to_beat(level_name_full, config_str,
ba.WeakCall(self._on_got_scores_to_beat))
@ -53,9 +39,10 @@ class CoopJoinActivity(JoinActivity):
from bastd.actor.controlsguide import ControlsGuide
from bastd.actor.text import Text
super().on_transition_in()
assert isinstance(self.session, ba.CoopSession)
assert self.session.campaign
Text(self.session.campaign.get_level(
self.session.campaign_state['level']).displayname,
Text(self.session.campaign.getlevel(
self.session.campaign_level_name).displayname,
scale=1.3,
h_attach=Text.HAttach.CENTER,
h_align=Text.HAlign.CENTER,
@ -70,12 +57,13 @@ class CoopJoinActivity(JoinActivity):
scores: Optional[List[Dict[str, Any]]]) -> None:
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
from efro.util import asserttype
from bastd.actor.text import Text
from ba.internal import get_achievements_for_coop_level
# Sort by originating date so that the most recent is first.
if scores is not None:
scores.sort(reverse=True, key=lambda score: score['time'])
scores.sort(reverse=True,
key=lambda score: asserttype(score['time'], int))
# We only show achievements and challenges for CoopGameActivities.
session = self.session
@ -163,13 +151,15 @@ class CoopJoinActivity(JoinActivity):
# Now list our remaining achievements for this level.
assert self.session.campaign is not None
assert isinstance(self.session, ba.CoopSession)
levelname = (self.session.campaign.name + ':' +
self.session.campaign_state['level'])
self.session.campaign_level_name)
ts_h_offs = 60
if not ba.app.kiosk_mode:
if not (ba.app.demo_mode or ba.app.arcade_mode):
achievements = [
a for a in get_achievements_for_coop_level(levelname)
a
for a in ba.app.ach.achievements_for_coop_level(levelname)
if not a.complete
]
have_achievements = bool(achievements)

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Provides a score screen for coop games."""
# pylint: disable=too-many-lines
@ -28,7 +10,6 @@ from typing import TYPE_CHECKING
import _ba
import ba
from ba.internal import get_achievements_for_coop_level
from bastd.actor.text import Text
from bastd.actor.zoomtext import ZoomText
@ -38,17 +19,17 @@ if TYPE_CHECKING:
from bastd.ui.league.rankbutton import LeagueRankButton
class CoopScoreScreen(ba.Activity):
class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
"""Score screen showing the results of a cooperative game."""
def __init__(self, settings: Dict[str, Any]):
def __init__(self, settings: dict):
# pylint: disable=too-many-statements
super().__init__(settings=settings)
super().__init__(settings)
# Keep prev activity alive while we fade in
self.transition_time = 0.5
self.inherits_tint = True
self.inherits_camera_vr_offset = True
self.inherits_vr_camera_offset = True
self.inherits_music = True
self.use_fixed_vr_overlay = True
@ -68,8 +49,8 @@ class CoopScoreScreen(ba.Activity):
self._campaign: ba.Campaign = settings['campaign']
self._have_achievements = bool(
get_achievements_for_coop_level(self._campaign.name + ':' +
settings['level']))
ba.app.ach.achievements_for_coop_level(self._campaign.name + ':' +
settings['level']))
self._account_type = (_ba.get_account_type() if
_ba.get_account_state() == 'signed_in' else None)
@ -137,7 +118,10 @@ class CoopScoreScreen(ba.Activity):
self._tournament_time_remaining_text: Optional[Text] = None
self._tournament_time_remaining_text_timer: Optional[ba.Timer] = None
self._player_info = settings['player_info']
self._playerinfos: List[ba.PlayerInfo] = settings['playerinfos']
assert isinstance(self._playerinfos, list)
assert (isinstance(i, ba.PlayerInfo) for i in self._playerinfos)
self._score: Optional[int] = settings['score']
assert isinstance(self._score, (int, type(None)))
@ -149,8 +133,8 @@ class CoopScoreScreen(ba.Activity):
self._score_order: str
if 'score_order' in settings:
if not settings['score_order'] in ['increasing', 'decreasing']:
raise Exception('Invalid score order: ' +
settings['score_order'])
raise ValueError('Invalid score order: ' +
settings['score_order'])
self._score_order = settings['score_order']
else:
self._score_order = 'increasing'
@ -159,8 +143,8 @@ class CoopScoreScreen(ba.Activity):
self._score_type: str
if 'score_type' in settings:
if not settings['score_type'] in ['points', 'time']:
raise Exception('Invalid score type: ' +
settings['score_type'])
raise ValueError('Invalid score type: ' +
settings['score_type'])
self._score_type = settings['score_type']
else:
self._score_type = 'points'
@ -171,7 +155,7 @@ class CoopScoreScreen(ba.Activity):
self._game_name_str = self._campaign.name + ':' + self._level_name
self._game_config_str = str(len(
self._player_info)) + 'p' + self._campaign.get_level(
self._playerinfos)) + 'p' + self._campaign.getlevel(
self._level_name).get_score_version_string().replace(' ', '_')
# If game-center/etc scores are available we show our friends'
@ -180,7 +164,7 @@ class CoopScoreScreen(ba.Activity):
self._game_name_str, self._game_config_str)
try:
self._old_best_rank = self._campaign.get_level(
self._old_best_rank = self._campaign.getlevel(
self._level_name).rating
except Exception:
self._old_best_rank = 0.0
@ -319,7 +303,6 @@ class CoopScoreScreen(ba.Activity):
def show_ui(self) -> None:
"""Show the UI for restarting, playing the next Level, etc."""
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
from bastd.ui.store.button import StoreButton
from bastd.ui.league.rankbutton import LeagueRankButton
@ -327,15 +310,7 @@ class CoopScoreScreen(ba.Activity):
# If there's no players left in the game, lets not show the UI
# (that would allow restarting the game with zero players, etc).
# Hmmm shouldn't need this try/except here i don't think.
try:
players = self.players
except Exception as exc:
print(('EXC bs_coop_game show_ui cant get '
'self.players; shouldn\'t happen:'), exc)
players = []
if not players:
if not self.players:
return
rootc = self._root_ui = ba.containerwidget(size=(0, 0),
@ -395,7 +370,8 @@ class CoopScoreScreen(ba.Activity):
else:
pass
show_next_button = self._is_more_levels and not ba.app.kiosk_mode
show_next_button = self._is_more_levels and not (ba.app.demo_mode
or ba.app.arcade_mode)
if not show_next_button:
h_offs += 70
@ -461,7 +437,7 @@ class CoopScoreScreen(ba.Activity):
self._corner_button_offs = (h_offs + 300.0 + 100.0 + x_offs_extra,
v_offs + 560.0)
if ba.app.kiosk_mode:
if ba.app.demo_mode or ba.app.arcade_mode:
self._league_rank_button = None
self._store_button_instance = None
else:
@ -510,7 +486,7 @@ class CoopScoreScreen(ba.Activity):
self._store_button_instance.set_position((pos_x + 100, pos_y))
def on_begin(self) -> None:
# FIXME: clean this up
# FIXME: Clean this up.
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
@ -519,8 +495,8 @@ class CoopScoreScreen(ba.Activity):
self._begin_time = ba.time()
# Calc whether the level is complete and other stuff.
levels = self._campaign.get_levels()
level = self._campaign.get_level(self._level_name)
levels = self._campaign.levels
level = self._campaign.getlevel(self._level_name)
self._was_complete = level.complete
self._is_complete = (self._was_complete or self._victory)
self._newly_complete = (self._is_complete and not self._was_complete)
@ -548,7 +524,7 @@ class CoopScoreScreen(ba.Activity):
ba.timer(1.0, ba.WeakCall(self.request_ui))
if (self._is_complete and self._victory and self._is_more_levels
and not ba.app.kiosk_mode):
and not (ba.app.demo_mode or ba.app.arcade_mode)):
Text(ba.Lstr(value='${A}:\n',
subs=[('${A}', ba.Lstr(resource='levelUnlockedText'))
]) if self._newly_complete else
@ -577,18 +553,18 @@ class CoopScoreScreen(ba.Activity):
ba.timer(5.2, ba.Call(ba.playsound, self._dingsound))
offs_x = -195
if len(self._player_info) > 1:
if len(self._playerinfos) > 1:
pstr = ba.Lstr(value='- ${A} -',
subs=[('${A}',
ba.Lstr(resource='multiPlayerCountText',
subs=[('${COUNT}',
str(len(self._player_info)))
str(len(self._playerinfos)))
]))])
else:
pstr = ba.Lstr(value='- ${A} -',
subs=[('${A}',
ba.Lstr(resource='singlePlayerCountText'))])
ZoomText(self._campaign.get_level(self._level_name).displayname,
ZoomText(self._campaign.getlevel(self._level_name).displayname,
maxwidth=800,
flash=False,
trail=False,
@ -633,7 +609,7 @@ class CoopScoreScreen(ba.Activity):
ba.pushcall(ba.WeakCall(self._show_fail))
self._name_str = name_str = ', '.join(
[p['name'] for p in self._player_info])
[p.name for p in self._playerinfos])
if self._show_friend_scores:
self._friends_loading_status = Text(
@ -656,19 +632,19 @@ class CoopScoreScreen(ba.Activity):
ba.timer(0.4, ba.WeakCall(self._play_drumroll))
# Add us to high scores, filter, and store.
our_high_scores_all = self._campaign.get_level(
our_high_scores_all = self._campaign.getlevel(
self._level_name).get_high_scores()
try:
our_high_scores = our_high_scores_all[str(len(self._player_info)) +
' Player']
except Exception:
our_high_scores = our_high_scores_all[str(len(self._player_info)) +
' Player'] = []
our_high_scores = our_high_scores_all.setdefault(
str(len(self._playerinfos)) + ' Player', [])
if self._score is not None:
our_score: Optional[list] = [
self._score, {
'players': self._player_info
'players': [{
'name': p.name,
'character': p.character
} for p in self._playerinfos]
}
]
our_high_scores.append(our_score)
@ -679,13 +655,13 @@ class CoopScoreScreen(ba.Activity):
our_high_scores.sort(reverse=self._score_order == 'increasing',
key=lambda x: x[0])
except Exception:
ba.print_exception('Error sorting scores')
print('our_high_scores:', our_high_scores)
ba.print_exception('Error sorting scores.')
print(f'our_high_scores: {our_high_scores}')
del our_high_scores[10:]
if self._score is not None:
sver = (self._campaign.get_level(
sver = (self._campaign.getlevel(
self._level_name).get_score_version_string())
_ba.add_transaction({
'type': 'SET_LEVEL_LOCAL_HIGH_SCORES',
@ -696,7 +672,7 @@ class CoopScoreScreen(ba.Activity):
})
if _ba.get_account_state() != 'signed_in':
# We expect this only in kiosk mode; complain otherwise.
if not ba.app.kiosk_mode:
if not (ba.app.demo_mode or ba.app.arcade_mode):
print('got not-signed-in at score-submit; unexpected')
if self._show_friend_scores:
ba.pushcall(ba.WeakCall(self._got_friend_score_results, None))
@ -783,7 +759,7 @@ class CoopScoreScreen(ba.Activity):
v_offs_extra = 20
v_offs_names = 0
scale = 1.0
p_count = len(self._player_info)
p_count = len(self._playerinfos)
h_offs_extra -= 75
if p_count > 1:
h_offs_extra -= 20
@ -800,9 +776,15 @@ class CoopScoreScreen(ba.Activity):
(1.9 + i * 0.05, 2.3 + i * 0.05))
for i in range(display_count):
try:
name_str = ', '.join(
[p['name'] for p in display_scores[i][1]['players']])
if display_scores[i][1] is None:
name_str = '-'
else:
name_str = ', '.join([
p['name'] for p in display_scores[i][1]['players']
])
except Exception:
ba.print_exception(
f'Error calcing name_str for {display_scores}')
name_str = '-'
if display_scores[i] == our_score and not showed_ours:
flash = True
@ -835,7 +817,7 @@ class CoopScoreScreen(ba.Activity):
position=(ts_h_offs + 35 + h_offs_extra,
v_offs_extra + ts_height / 2 + -ts_height *
(i + 1) / 10 + v_offs_names + v_offs + 11.0),
maxwidth=80.0 + 100.0 * len(self._player_info),
maxwidth=80.0 + 100.0 * len(self._playerinfos),
v_align=Text.VAlign.CENTER,
color=color1,
flash=flash,
@ -864,7 +846,8 @@ class CoopScoreScreen(ba.Activity):
transition_delay=2.8).autoretain()
assert self._game_name_str is not None
achievements = get_achievements_for_coop_level(self._game_name_str)
achievements = ba.app.ach.achievements_for_coop_level(
self._game_name_str)
hval = -455
vval = -100
tdelay = 0.0
@ -890,6 +873,7 @@ class CoopScoreScreen(ba.Activity):
# pylint: disable=too-many-locals
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
from efro.util import asserttype
# 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))
@ -926,7 +910,7 @@ class CoopScoreScreen(ba.Activity):
break
results.append(our_score_entry)
results.sort(reverse=self._score_order == 'increasing',
key=lambda x: x[0])
key=lambda x: asserttype(x[0], int))
# If we're not submitting our own score, we still want to change the
# name of our own score to 'Me'.
@ -1008,7 +992,7 @@ class CoopScoreScreen(ba.Activity):
# 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.is_expired():
if self.expired:
return
with ba.Context(self):
# Delay a bit if results come in too fast.
@ -1080,7 +1064,7 @@ class CoopScoreScreen(ba.Activity):
h_offs_extra = 0
v_offs_names = 0
scale = 1.0
p_count = len(self._player_info)
p_count = len(self._playerinfos)
if p_count > 1:
h_offs_extra -= 40
if self._score_type != 'points':
@ -1140,7 +1124,7 @@ class CoopScoreScreen(ba.Activity):
position=(ts_h_offs + 35 + h_offs_extra,
ts_height / 2 + -ts_height * (i + 1) / 10 +
v_offs_names + v_offs + 11.0),
maxwidth=80.0 + 100.0 * len(self._player_info),
maxwidth=80.0 + 100.0 * len(self._playerinfos),
v_align=Text.VAlign.CENTER,
color=color1,
flash=flash,
@ -1177,8 +1161,8 @@ class CoopScoreScreen(ba.Activity):
if 'error' in self._show_info['results'] else None)
rank = self._show_info['results']['rank']
total = self._show_info['results']['total']
rating = 10.0 if total == 1 else round(
10.0 * (1.0 - (float(rank - 1) / (total - 1))), 1)
rating = (10.0 if total == 1 else 10.0 * (1.0 - (float(rank - 1) /
(total - 1))))
player_rank = self._show_info['results']['playerRank']
best_player_rank = self._show_info['results']['bestPlayerRank']
else:
@ -1213,8 +1197,9 @@ class CoopScoreScreen(ba.Activity):
try:
tournament_id = self.session.tournament_id
if tournament_id is not None:
if tournament_id in ba.app.tournament_info:
tourney_info = ba.app.tournament_info[tournament_id]
if tournament_id in ba.app.accounts.tournament_info:
tourney_info = ba.app.accounts.tournament_info[
tournament_id]
# pylint: disable=unbalanced-tuple-unpacking
pr1, pv1, pr2, pv2, pr3, pv3 = (
get_tournament_prize_strings(tourney_info))
@ -1250,7 +1235,7 @@ class CoopScoreScreen(ba.Activity):
transition_delay=2.0).autoretain()
vval -= 35
except Exception:
ba.print_exception('error showing prize ranges')
ba.print_exception('Error showing prize ranges.')
if self._do_new_rating:
if error:
@ -1303,7 +1288,7 @@ class CoopScoreScreen(ba.Activity):
scale=0.7,
transition_delay=1.0).autoretain()
else:
ZoomText((str(rating) if available else ba.Lstr(
ZoomText((f'{rating:.1f}' if available else ba.Lstr(
resource='unavailableText')),
flash=True,
trail=True,
@ -1385,7 +1370,7 @@ class CoopScoreScreen(ba.Activity):
dostar(2, 10 - 30, -112, '7.5')
dostar(3, 77 - 30, -112, '9.5')
try:
best_rank = self._campaign.get_level(self._level_name).rating
best_rank = self._campaign.getlevel(self._level_name).rating
except Exception:
best_rank = 0.0

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality related to the draw screen."""
from __future__ import annotations
@ -26,6 +8,7 @@ from typing import TYPE_CHECKING
import ba
from bastd.activity.multiteamscore import MultiTeamScoreScreenActivity
from bastd.actor.zoomtext import ZoomText
if TYPE_CHECKING:
from typing import Any, Dict
@ -34,15 +17,9 @@ if TYPE_CHECKING:
class DrawScoreScreenActivity(MultiTeamScoreScreenActivity):
"""Score screen shown after a draw."""
def __init__(self, settings: Dict[str, Any]):
super().__init__(settings=settings)
def on_transition_in(self) -> None:
self.default_music = None # Awkward silence...
super().on_transition_in()
default_music = None # Awkward silence...
def on_begin(self) -> None:
from bastd.actor.zoomtext import ZoomText
ba.set_analytics_screen('Draw Score Screen')
super().on_begin()
ZoomText(ba.Lstr(resource='drawText'),
@ -54,4 +31,4 @@ class DrawScoreScreenActivity(MultiTeamScoreScreenActivity):
trail=False,
jitter=1.0).autoretain()
ba.timer(0.35, ba.Call(ba.playsound, self._score_display_sound))
self.show_player_scores(results=self.settings.get('results', None))
self.show_player_scores(results=self.settings_raw.get('results', None))

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality related to the end screen in dual-team mode."""
from __future__ import annotations
@ -35,11 +17,12 @@ if TYPE_CHECKING:
class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
"""Scorescreen between rounds of a dual-team session."""
def __init__(self, settings: Dict[str, Any]):
def __init__(self, settings: dict):
super().__init__(settings=settings)
self._winner: ba.SessionTeam = settings['winner']
assert isinstance(self._winner, ba.SessionTeam)
def on_begin(self) -> None:
from ba.deprecated import get_resource
ba.set_analytics_screen('Teams Score Screen')
super().on_begin()
@ -53,7 +36,7 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
# 'First to 4'.
session = self.session
assert isinstance(session, ba.MultiTeamSession)
if get_resource('bestOfUseFirstToInstead'):
if ba.app.lang.get_resource('bestOfUseFirstToInstead'):
best_txt = ba.Lstr(resource='firstToSeriesText',
subs=[('${COUNT}',
str(session.get_series_length() / 2 + 1))
@ -73,14 +56,14 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
scale=0.25,
color=(0.5, 0.5, 0.5, 1.0),
jitter=3.0).autoretain()
for team in self.teams:
for team in self.session.sessionteams:
ba.timer(
i * 0.15 + 0.15,
ba.WeakCall(self._show_team_name, vval - i * height, team,
i * 0.2, shift_time - (i * 0.150 + 0.150)))
ba.timer(i * 0.150 + 0.5,
ba.Call(ba.playsound, self._score_display_sound_small))
scored = (team is self.settings['winner'])
scored = (team is self._winner)
delay = 0.2
if scored:
delay = 1.2
@ -99,9 +82,9 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
i += 1
self.show_player_scores()
def _show_team_name(self, pos_v: float, team: ba.Team, kill_delay: float,
shiftdelay: float) -> None:
del kill_delay # unused arg
def _show_team_name(self, pos_v: float, team: ba.SessionTeam,
kill_delay: float, shiftdelay: float) -> None:
del kill_delay # Unused arg.
ZoomText(ba.Lstr(value='${A}:', subs=[('${A}', team.name)]),
position=(100, pos_v),
shiftposition=(-150, pos_v),
@ -113,9 +96,9 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
color=team.color,
jitter=1.0).autoretain()
def _show_team_old_score(self, pos_v: float, team: ba.Team,
def _show_team_old_score(self, pos_v: float, sessionteam: ba.SessionTeam,
shiftdelay: float) -> None:
ZoomText(str(team.sessiondata['score'] - 1),
ZoomText(str(sessionteam.customdata['score'] - 1),
position=(150, pos_v),
maxwidth=100,
color=(0.6, 0.6, 0.7),
@ -127,10 +110,11 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
h_align='left',
jitter=1.0).autoretain()
def _show_team_score(self, pos_v: float, team: ba.Team, scored: bool,
kill_delay: float, shiftdelay: float) -> None:
del kill_delay # unused arg
ZoomText(str(team.sessiondata['score']),
def _show_team_score(self, pos_v: float, sessionteam: ba.SessionTeam,
scored: bool, kill_delay: float,
shiftdelay: float) -> None:
del kill_delay # Unused arg.
ZoomText(str(sessionteam.customdata['score']),
position=(150, pos_v),
maxwidth=100,
color=(1.0, 0.9, 0.5) if scored else (0.6, 0.6, 0.7),

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality related to the final screen in free-for-all games."""
from __future__ import annotations
@ -28,13 +10,13 @@ import ba
from bastd.activity.multiteamscore import MultiTeamScoreScreenActivity
if TYPE_CHECKING:
from typing import Any, Dict, Optional, Set
from typing import Any, Dict, Optional, Set, Tuple
class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
"""Score screen shown at after free-for-all rounds."""
def __init__(self, settings: Dict[str, Any]):
def __init__(self, settings: dict):
super().__init__(settings=settings)
# Keep prev activity alive while we fade in.
@ -60,13 +42,17 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
player_order_prev = list(self.players)
player_order_prev.sort(
reverse=True,
key=lambda p:
(p.team.sessiondata['previous_score'], p.get_name(full=True)))
key=lambda p: (
p.team.sessionteam.customdata['previous_score'],
p.getname(full=True),
))
player_order = list(self.players)
player_order.sort(reverse=True,
key=lambda p:
(p.team.sessiondata['score'], p.team.sessiondata[
'score'], p.get_name(full=True)))
key=lambda p: (
p.team.sessionteam.customdata['score'],
p.team.sessionteam.customdata['score'],
p.getname(full=True),
))
v_offs = -74.0 + spacing * len(player_order_prev) * 0.5
delay1 = 1.3 + 0.1
@ -78,8 +64,8 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
delay3 += 1.5
ba.timer(0.3, ba.Call(ba.playsound, self._score_display_sound))
results = self.settings['results']
assert isinstance(results, ba.TeamGameResults)
results = self.settings_raw['results']
assert isinstance(results, ba.GameResults)
self.show_player_scores(delay=0.001,
results=results,
scale=1.2,
@ -161,7 +147,7 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
0: ts_h_offs - 72.0 * scale,
transtime2: ts_h_offs - (72.0 + slide_amt) * scale
}))
txt = Text(ba.Lstr(value=player.get_name(full=True)),
txt = Text(ba.Lstr(value=player.getname(full=True)),
maxwidth=130.0,
scale=0.75 * scale,
position=(ts_h_offs - 50.0 * scale,
@ -202,8 +188,9 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
transtime2: ts_h_offs - (95.0 + slide_amt) * scale
}))
s_txt = _scoretxt(str(player.team.sessiondata['previous_score']),
80, 0, False, 0, 1.0)
s_txt = _scoretxt(
str(player.team.sessionteam.customdata['previous_score']), 80,
0, False, 0, 1.0)
ba.timer(
tdelay + delay2,
ba.WeakCall(
@ -219,8 +206,9 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
transtime2: ts_h_offs + (80.0 - slide_amt) * scale
}))
score_change = (player.team.sessiondata['score'] -
player.team.sessiondata['previous_score'])
score_change = (
player.team.sessionteam.customdata['score'] -
player.team.sessionteam.customdata['previous_score'])
if score_change > 0:
xval = 113
yval = 3.0
@ -257,12 +245,11 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
tdelay + delay1,
ba.Call(_safesetattr, s_txt.node, 'color', (1, 1, 1, 1)))
for j in range(score_change):
ba.timer(
(tdelay + delay1 + 0.15 * j),
ba.Call(
_safesetattr, s_txt.node, 'text',
str(player.team.sessiondata['previous_score'] + j +
1)))
ba.timer((tdelay + delay1 + 0.15 * j),
ba.Call(
_safesetattr, s_txt.node, 'text',
str(player.team.sessionteam.
customdata['previous_score'] + j + 1)))
tfin = tdelay + delay1 + 0.15 * j
if tfin not in sound_times:
sound_times.add(tfin)

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality related to the join screen for multi-team sessions."""
from __future__ import annotations
@ -35,7 +17,7 @@ if TYPE_CHECKING:
class MultiTeamJoinActivity(JoinActivity):
"""Join screen for teams sessions."""
def __init__(self, settings: Dict[str, Any]):
def __init__(self, settings: dict):
super().__init__(settings)
self._next_up_text: Optional[Text] = None
@ -66,9 +48,10 @@ 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().teams]
team_names = [team.name for team in ba.getsession().sessionteams]
team_colors = [
tuple(team.color) + (0.5, ) for team in ba.getsession().teams
tuple(team.color) + (0.5, )
for team in ba.getsession().sessionteams
]
if len(team_names) == 2:
for i in range(2):

View File

@ -1,31 +1,14 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality related to teams mode score screen."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
from ba.internal import ScoreScreenActivity
from bastd.actor.text import Text
from bastd.actor.image import Image
if TYPE_CHECKING:
from typing import Any, Dict, Optional, Union
@ -34,7 +17,7 @@ if TYPE_CHECKING:
class MultiTeamScoreScreenActivity(ScoreScreenActivity):
"""Base class for score screens."""
def __init__(self, settings: Dict[str, Any]):
def __init__(self, settings: dict):
super().__init__(settings=settings)
self._score_display_sound = ba.getsound('scoreHit01')
self._score_display_sound_small = ba.getsound('scoreHit02')
@ -42,7 +25,6 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
self._show_up_next: bool = True
def on_begin(self) -> None:
from bastd.actor.text import Text
super().on_begin()
session = self.session
if self._show_up_next and isinstance(session, ba.MultiTeamSession):
@ -70,15 +52,13 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
def show_player_scores(self,
delay: float = 2.5,
results: Optional[ba.TeamGameResults] = None,
results: Optional[ba.GameResults] = None,
scale: float = 1.0,
x_offset: float = 0.0,
y_offset: float = 0.0) -> None:
"""Show scores for individual players."""
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
from bastd.actor.text import Text
from bastd.actor.image import Image
ts_v_offset = 150.0 + y_offset
ts_h_offs = 80.0 + x_offset
@ -89,15 +69,17 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
def _get_prec_score(p_rec: ba.PlayerRecord) -> Optional[int]:
if is_free_for_all and results is not None:
assert isinstance(results, ba.TeamGameResults)
val = results.get_team_score(p_rec.team)
assert isinstance(results, ba.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) -> Union[str, ba.Lstr]:
if is_free_for_all and results is not None:
assert isinstance(results, ba.TeamGameResults)
val = results.get_team_score_str(p_rec.team)
assert isinstance(results, ba.GameResults)
assert p_rec.team.activityteam is not None
val = results.get_sessionteam_score_str(p_rec.team)
assert val is not None
return val
return str(p_rec.accumscore)
@ -107,24 +89,22 @@ 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.TeamGameResults)
assert isinstance(results, ba.GameResults)
player_records = []
assert self.stats
valid_players = list(self.stats.get_records().items())
def _get_player_score_set_entry(
player: ba.Player) -> Optional[ba.PlayerRecord]:
player: ba.SessionPlayer) -> Optional[ba.PlayerRecord]:
for p_rec in valid_players:
# PyCharm incorrectly thinks valid_players is a List[str]
# noinspection PyUnresolvedReferences
if p_rec[1].player is player:
return p_rec[1]
return None
# Results is already sorted; just convert it into a list of
# score-set-entries.
for winner in results.get_winners():
for team in winner.teams:
for winnergroup in results.winnergroups:
for team in winnergroup.teams:
if len(team.players) == 1:
player_entry = _get_player_score_set_entry(
team.players[0])
@ -141,18 +121,18 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
# Just want living player entries.
player_records = [p[2] for p in player_records_scores if p[2]]
v_offs = -140.0 + spacing * len(player_records) * 0.5
voffs = -140.0 + spacing * len(player_records) * 0.5
def _txt(x_offs: float,
y_offs: float,
def _txt(xoffs: float,
yoffs: float,
text: ba.Lstr,
h_align: Text.HAlign = Text.HAlign.RIGHT,
extrascale: float = 1.0,
maxwidth: Optional[float] = 120.0) -> None:
Text(text,
color=(0.5, 0.5, 0.6, 0.5),
position=(ts_h_offs + x_offs * scale,
ts_v_offset + (v_offs + y_offs + 4.0) * scale),
position=(ts_h_offs + xoffs * scale,
ts_v_offset + (voffs + yoffs + 4.0) * scale),
h_align=h_align,
v_align=Text.VAlign.CENTER,
scale=0.8 * scale * extrascale,
@ -174,8 +154,8 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
_txt(180, 4, ba.Lstr(resource='killsText'))
_txt(280, 4, ba.Lstr(resource='deathsText'), maxwidth=100)
score_name = 'Score' if results is None else results.get_score_name()
translated = ba.Lstr(translate=('scoreNames', score_name))
score_label = 'Score' if results is None else results.score_label
translated = ba.Lstr(translate=('scoreNames', score_label))
_txt(390, 0, translated)
@ -195,7 +175,7 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
maxwidth: float = 70.0) -> None:
Text(text,
position=(ts_h_offs + x_offs * scale,
ts_v_offset + (v_offs + 15) * scale),
ts_v_offset + (voffs + 15) * scale),
scale=scale,
color=(1.0, 0.9, 0.5, 1.0) if highlight else
(0.5, 0.5, 0.6, 0.5),
@ -207,18 +187,18 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
for playerrec in player_records:
tdelay += 0.05
v_offs -= spacing
voffs -= spacing
Image(playerrec.get_icon(),
position=(ts_h_offs - 12 * scale,
ts_v_offset + (v_offs + 15.0) * scale),
ts_v_offset + (voffs + 15.0) * scale),
scale=(30.0 * scale, 30.0 * scale),
transition=Image.Transition.IN_LEFT,
transition_delay=tdelay).autoretain()
Text(ba.Lstr(value=playerrec.get_name(full=True)),
Text(ba.Lstr(value=playerrec.getname(full=True)),
maxwidth=160,
scale=0.75 * scale,
position=(ts_h_offs + 10.0 * scale,
ts_v_offset + (v_offs + 15) * scale),
ts_v_offset + (voffs + 15) * scale),
h_align=Text.HAlign.LEFT,
v_align=Text.VAlign.CENTER,
color=ba.safecolor(playerrec.team.color + (1, )),

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality related to the final screen in multi-teams sessions."""
from __future__ import annotations
@ -34,18 +16,16 @@ if TYPE_CHECKING:
class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
"""Final score screen for a team series."""
def __init__(self, settings: Dict[str, Any]):
# Dont' play music by default; (we do manually after a delay).
default_music = None
def __init__(self, settings: dict):
super().__init__(settings=settings)
self._min_view_time = 15.0
self._is_ffa = isinstance(self.session, ba.FreeForAllSession)
self._allow_server_transition = True
self._tips_text = None
def on_transition_in(self) -> None:
# We don't yet want music and whatnot...
self.default_music = None
self._default_show_tips = False
super().on_transition_in()
def on_begin(self) -> None:
# pylint: disable=too-many-branches
@ -53,21 +33,21 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
# pylint: disable=too-many-statements
from bastd.actor.text import Text
from bastd.actor.image import Image
from ba.deprecated import get_resource
ba.set_analytics_screen('FreeForAll Series Victory Screen' if self.
_is_ffa else 'Teams Series Victory Screen')
if ba.app.interface_type == 'large':
if ba.app.ui.uiscale is ba.UIScale.LARGE:
sval = ba.Lstr(resource='pressAnyKeyButtonPlayAgainText')
else:
sval = ba.Lstr(resource='pressAnyButtonPlayAgainText')
self._show_up_next = False
self._custom_continue_message = sval
super().on_begin()
winning_team = self.settings['winner']
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['winner']))
ba.timer(4.4,
ba.WeakCall(self._show_winner, self.settings_raw['winner']))
ba.timer(4.6, ba.Call(ba.playsound, self._score_display_sound))
# Score / Name / Player-record.
@ -78,8 +58,8 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
for _pkey, prec in self.stats.get_records().items():
if prec.player.in_game:
player_entries.append(
(prec.player.team.sessiondata['score'],
prec.get_name(full=True), prec))
(prec.player.sessionteam.customdata['score'],
prec.getname(full=True), prec))
player_entries.sort(reverse=True, key=lambda x: x[0])
else:
for _pkey, prec in self.stats.get_records().items():
@ -91,7 +71,8 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
tval = 6.4
t_incr = 0.12
always_use_first_to = get_resource('bestOfUseFirstToInstead')
always_use_first_to = ba.app.lang.get_resource(
'bestOfUseFirstToInstead')
session = self.session
if self._is_ffa:
@ -141,11 +122,11 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
h_align=Text.HAlign.CENTER,
transition_delay=t_incr * 4).autoretain()
win_score = (session.get_series_length() - 1) / 2 + 1
win_score = (session.get_series_length() - 1) // 2 + 1
lose_score = 0
for team in self.teams:
if team.sessiondata['score'] != win_score:
lose_score = team.sessiondata['score']
if team.sessionteam.customdata['score'] != win_score:
lose_score = team.sessionteam.customdata['score']
if not self._is_ffa:
Text(ba.Lstr(resource='gamesToText',
@ -172,7 +153,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
if not self._is_ffa:
mvp, mvp_name = None, None
for entry in player_entries:
if entry[2].team == winning_team:
if entry[2].team == winning_sessionteam:
mvp = entry[2]
mvp_name = entry[1]
break
@ -308,7 +289,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
for _score, name, prec in player_entries:
tdelay -= 4 * t_incr
v_offs -= 40
Text(str(prec.team.sessiondata['score'])
Text(str(prec.team.customdata['score'])
if self._is_ffa else str(prec.score),
color=(0.5, 0.5, 0.5, 1.0),
position=(ts_h_offs + 230, ts_height / 2 + v_offs),
@ -343,7 +324,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
if not self.is_transitioning_out():
ba.setmusic(ba.MusicType.VICTORY)
def _show_winner(self, team: ba.Team) -> None:
def _show_winner(self, team: ba.SessionTeam) -> None:
from bastd.actor.image import Image
from bastd.actor.zoomtext import ZoomText
if not self._is_ffa:
@ -363,7 +344,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
assert i.node
ba.animate(i.node, 'opacity', {0.0: 0.0, 0.25: 1.0})
ZoomText(ba.Lstr(
value=team.players[0].get_name(full=True, icon=False)),
value=team.players[0].getname(full=True, icon=False)),
position=(0, 97 + offs_v),
color=team.color,
scale=1.15,

View File

@ -1,20 +1 @@
# Copyright (c) 2011-2020 Eric Froemling
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
# Released under the MIT License. See LICENSE for details.

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Defines Actor(s)."""
from __future__ import annotations
@ -150,8 +132,7 @@ class Background(ba.Actor):
ba.timer(self.fade_time + 0.1, self.node.delete)
def handlemessage(self, msg: Any) -> Any:
if __debug__:
self._handlemessage_sanity_check()
assert not self.expired
if isinstance(msg, ba.DieMessage):
self._die(msg.immediate)
else:

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Various classes for bombs, mines, tnt, etc."""
# FIXME
@ -26,12 +8,15 @@
from __future__ import annotations
import random
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, TypeVar
import ba
from bastd.gameutils import SharedObjects
if TYPE_CHECKING:
from typing import Any, Sequence, Optional, Callable, List, Tuple
from typing import Any, Sequence, Optional, Callable, List, Tuple, Type
PlayerType = TypeVar('PlayerType', bound='ba.Player')
class BombFactory:
@ -143,6 +128,19 @@ class BombFactory:
ba.Sound for a rolling bomb.
"""
_STORENAME = ba.storagename()
@classmethod
def get(cls) -> BombFactory:
"""Get/create a shared bastd.actor.bomb.BombFactory object."""
activity = ba.getactivity()
factory = activity.customdata.get(cls._STORENAME)
if factory is None:
factory = BombFactory()
activity.customdata[cls._STORENAME] = factory
assert isinstance(factory, BombFactory)
return factory
def random_explode_sound(self) -> ba.Sound:
"""Return a random explosion ba.Sound from the factory."""
return self.explode_sounds[random.randrange(len(self.explode_sounds))]
@ -153,6 +151,7 @@ class BombFactory:
You shouldn't need to do this; call bastd.actor.bomb.get_factory()
to get a shared instance.
"""
shared = SharedObjects.get()
self.bomb_model = ba.getmodel('bomb')
self.sticky_bomb_model = ba.getmodel('bombSticky')
@ -184,24 +183,31 @@ class BombFactory:
self.activate_sound = ba.getsound('activateBeep')
self.warn_sound = ba.getsound('warnBeep')
# set up our material so new bombs don't collide with objects
# that they are initially overlapping
# 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.add_actions(
conditions=((('we_are_younger_than', 100), 'or',
('they_are_younger_than', 100)),
'and', ('they_have_material',
ba.sharedobj('object_material'))),
actions=('modify_node_collision', 'collide', False))
conditions=(
(
('we_are_younger_than', 100),
'or',
('they_are_younger_than', 100),
),
'and',
('they_have_material', shared.object_material),
),
actions=('modify_node_collision', 'collide', False),
)
# we want pickup materials to always hit us even if we're currently not
# colliding with their node (generally due to the above rule)
# We want pickup materials to always hit us even if we're currently
# not colliding with their node. (generally due to the above rule)
self.bomb_material.add_actions(
conditions=('they_have_material', ba.sharedobj('pickup_material')),
actions=('modify_part_collision', 'use_node_collide', False))
conditions=('they_have_material', shared.pickup_material),
actions=('modify_part_collision', 'use_node_collide', False),
)
self.bomb_material.add_actions(actions=('modify_part_collision',
'friction', 0.3))
@ -209,48 +215,67 @@ class BombFactory:
self.land_mine_no_explode_material = ba.Material()
self.land_mine_blast_material = ba.Material()
self.land_mine_blast_material.add_actions(
conditions=(('we_are_older_than',
200), 'and', ('they_are_older_than',
200), 'and', ('eval_colliding', ),
'and', (('they_dont_have_material',
self.land_mine_no_explode_material), 'and',
(('they_have_material',
ba.sharedobj('object_material')), 'or',
('they_have_material',
ba.sharedobj('player_material'))))),
actions=('message', 'our_node', 'at_connect', ImpactMessage()))
conditions=(
('we_are_older_than', 200),
'and',
('they_are_older_than', 200),
'and',
('eval_colliding', ),
'and',
(
('they_dont_have_material',
self.land_mine_no_explode_material),
'and',
(
('they_have_material', shared.object_material),
'or',
('they_have_material', shared.player_material),
),
),
),
actions=('message', 'our_node', 'at_connect', ImpactMessage()),
)
self.impact_blast_material = ba.Material()
self.impact_blast_material.add_actions(
conditions=(('we_are_older_than',
200), 'and', ('they_are_older_than',
200), 'and', ('eval_colliding', ),
'and', (('they_have_material',
ba.sharedobj('footing_material')), 'or',
('they_have_material',
ba.sharedobj('object_material')))),
actions=('message', 'our_node', 'at_connect', ImpactMessage()))
conditions=(
('we_are_older_than', 200),
'and',
('they_are_older_than', 200),
'and',
('eval_colliding', ),
'and',
(
('they_have_material', shared.footing_material),
'or',
('they_have_material', shared.object_material),
),
),
actions=('message', 'our_node', 'at_connect', ImpactMessage()),
)
self.blast_material = ba.Material()
self.blast_material.add_actions(
conditions=(('they_have_material',
ba.sharedobj('object_material'))),
actions=(('modify_part_collision', 'collide', True),
('modify_part_collision', 'physical',
False), ('message', 'our_node', 'at_connect',
ExplodeHitMessage())))
conditions=('they_have_material', shared.object_material),
actions=(
('modify_part_collision', 'collide', True),
('modify_part_collision', 'physical', False),
('message', 'our_node', 'at_connect', ExplodeHitMessage()),
),
)
self.dink_sounds = (ba.getsound('bombDrop01'),
ba.getsound('bombDrop02'))
self.sticky_impact_sound = ba.getsound('stickyImpact')
self.roll_sound = ba.getsound('bombRoll01')
# collision sounds
# Collision sounds.
self.normal_sound_material.add_actions(
conditions=('they_have_material',
ba.sharedobj('footing_material')),
actions=(('impact_sound', self.dink_sounds, 2, 0.8),
('roll_sound', self.roll_sound, 3, 6)))
conditions=('they_have_material', shared.footing_material),
actions=(
('impact_sound', self.dink_sounds, 2, 0.8),
('roll_sound', self.roll_sound, 3, 6),
))
self.sticky_material.add_actions(actions=(('modify_part_collision',
'stiffness', 0.1),
@ -258,27 +283,13 @@ class BombFactory:
'damping', 1.0)))
self.sticky_material.add_actions(
conditions=(('they_have_material',
ba.sharedobj('player_material')),
'or', ('they_have_material',
ba.sharedobj('footing_material'))),
actions=('message', 'our_node', 'at_connect', SplatMessage()))
# noinspection PyTypeHints
def get_factory() -> BombFactory:
"""Get/create a shared bastd.actor.bomb.BombFactory object."""
activity = ba.getactivity()
# FIXME: Need to figure out an elegant way to store
# shared actor data with an activity.
factory: BombFactory
try:
factory = activity.shared_bomb_factory # type: ignore
except Exception:
factory = activity.shared_bomb_factory = BombFactory() # type: ignore
assert isinstance(factory, BombFactory)
return factory
conditions=(
('they_have_material', shared.player_material),
'or',
('they_have_material', shared.footing_material),
),
actions=('message', 'our_node', 'at_connect', SplatMessage()),
)
class SplatMessage:
@ -304,9 +315,6 @@ class WarnMessage:
class ExplodeHitMessage:
"""Tell an object it was hit by an explosion."""
def __init__(self) -> None:
pass
class Blast(ba.Actor):
"""An explosion, as generated by a bomb or some other object.
@ -330,30 +338,31 @@ class Blast(ba.Actor):
super().__init__()
factory = get_factory()
shared = SharedObjects.get()
factory = BombFactory.get()
self.blast_type = blast_type
self.source_player = source_player
self._source_player = source_player
self.hit_type = hit_type
self.hit_subtype = hit_subtype
self.radius = blast_radius
# set our position a bit lower so we throw more things upward
rmats = (factory.blast_material, ba.sharedobj('attack_material'))
self.node = ba.newnode('region',
delegate=self,
attrs={
'position': (position[0], position[1] - 0.1,
position[2]),
'scale':
(self.radius, self.radius, self.radius),
'type': 'sphere',
'materials': rmats
})
# Set our position a bit lower so we throw more things upward.
rmats = (factory.blast_material, shared.attack_material)
self.node = ba.newnode(
'region',
delegate=self,
attrs={
'position': (position[0], position[1] - 0.1, position[2]),
'scale': (self.radius, self.radius, self.radius),
'type': 'sphere',
'materials': rmats
},
)
ba.timer(0.05, self.node.delete)
# throw in an explosion and flash
# Throw in an explosion and flash.
evel = (velocity[0], max(-1.0, velocity[1]), velocity[2])
explosion = ba.newnode('explosion',
attrs={
@ -382,7 +391,7 @@ class Blast(ba.Actor):
emit_type='distortion',
spread=1.0 if self.blast_type == 'tnt' else 2.0)
# and emit some shrapnel..
# And emit some shrapnel.
if self.blast_type == 'ice':
def emit() -> None:
@ -394,7 +403,7 @@ class Blast(ba.Actor):
chunk_type='ice',
emit_type='stickers')
# looks better if we delay a bit
# It looks better if we delay a bit.
ba.timer(0.05, emit)
elif self.blast_type == 'sticky':
@ -430,10 +439,10 @@ class Blast(ba.Actor):
spread=1.5,
chunk_type='spark')
# looks better if we delay a bit
# It looks better if we delay a bit.
ba.timer(0.05, emit)
elif self.blast_type == 'impact': # regular bomb shrapnel
elif self.blast_type == 'impact':
def emit() -> None:
ba.emitfx(position=position,
@ -459,10 +468,10 @@ class Blast(ba.Actor):
spread=1.5,
chunk_type='spark')
# looks better if we delay a bit
# It looks better if we delay a bit.
ba.timer(0.05, emit)
else: # regular or land mine bomb shrapnel
else: # Regular or land mine bomb shrapnel.
def emit() -> None:
if self.blast_type != 'tnt':
@ -488,7 +497,7 @@ class Blast(ba.Actor):
spread=1.5,
chunk_type='spark')
# tnt throws splintery chunks
# TNT throws splintery chunks.
if self.blast_type == 'tnt':
def emit_splinters() -> None:
@ -501,7 +510,7 @@ class Blast(ba.Actor):
ba.timer(0.01, emit_splinters)
# every now and then do a sparky one
# Every now and then do a sparky one.
if self.blast_type == 'tnt' or random.random() < 0.1:
def emit_extra_sparks() -> None:
@ -514,7 +523,7 @@ class Blast(ba.Actor):
ba.timer(0.02, emit_extra_sparks)
# looks better if we delay a bit
# It looks better if we delay a bit.
ba.timer(0.05, emit)
lcolor = ((0.6, 0.6, 1.0) if self.blast_type == 'ice' else
@ -556,7 +565,7 @@ class Blast(ba.Actor):
})
ba.timer(scl * 3.0, light.delete)
# make a scorch that fades over time
# Make a scorch that fades over time.
scorch = ba.newnode('scorch',
attrs={
'position': position,
@ -578,7 +587,7 @@ class Blast(ba.Actor):
ba.camerashake(intensity=5.0 if self.blast_type == 'tnt' else 1.0)
# tnt is more epic..
# TNT is more epic.
if self.blast_type == 'tnt':
ba.playsound(factory.random_explode_sound(), position=lpos)
@ -594,44 +603,41 @@ class Blast(ba.Actor):
ba.timer(0.4, _extra_debris_sound)
def handlemessage(self, msg: Any) -> Any:
if __debug__:
self._handlemessage_sanity_check()
assert not self.expired
if isinstance(msg, ba.DieMessage):
if self.node:
self.node.delete()
elif isinstance(msg, ExplodeHitMessage):
node = ba.get_collision_info('opposing_node')
if node:
assert self.node
nodepos = self.node.position
node = ba.getcollision().opposingnode
assert self.node
nodepos = self.node.position
mag = 2000.0
if self.blast_type == 'ice':
mag *= 0.5
elif self.blast_type == 'land_mine':
mag *= 2.5
elif self.blast_type == 'tnt':
mag *= 2.0
# new
mag = 2000.0
if self.blast_type == 'ice':
mag *= 0.5
elif self.blast_type == 'land_mine':
mag *= 2.5
elif self.blast_type == 'tnt':
mag *= 2.0
node.handlemessage(
ba.HitMessage(pos=nodepos,
velocity=(0, 0, 0),
magnitude=mag,
hit_type=self.hit_type,
hit_subtype=self.hit_subtype,
radius=self.radius,
source_player=self.source_player))
if self.blast_type == 'ice':
ba.playsound(get_factory().freeze_sound,
10,
position=nodepos)
node.handlemessage(ba.FreezeMessage())
node.handlemessage(
ba.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)))
if self.blast_type == 'ice':
ba.playsound(BombFactory.get().freeze_sound,
10,
position=nodepos)
node.handlemessage(ba.FreezeMessage())
else:
super().handlemessage(msg)
return super().handlemessage(msg)
return None
class Bomb(ba.Actor):
@ -640,7 +646,7 @@ class Bomb(ba.Actor):
category: Gameplay Classes
"""
# Ew; should try to clean this up later
# Ew; should try to clean this up later.
# pylint: disable=too-many-locals
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
@ -650,6 +656,7 @@ class Bomb(ba.Actor):
velocity: Sequence[float] = (0.0, 0.0, 0.0),
bomb_type: str = 'normal',
blast_radius: float = 2.0,
bomb_scale: float = 1.0,
source_player: ba.Player = None,
owner: ba.Node = None):
"""Create a new Bomb.
@ -660,14 +667,16 @@ class Bomb(ba.Actor):
"""
super().__init__()
factory = get_factory()
shared = SharedObjects.get()
factory = BombFactory.get()
if bomb_type not in ('ice', 'impact', 'land_mine', 'normal', 'sticky',
'tnt'):
raise Exception('invalid bomb type: ' + bomb_type)
raise ValueError('invalid bomb type: ' + bomb_type)
self.bomb_type = bomb_type
self._exploded = False
self.scale = bomb_scale
self.texture_sequence: Optional[ba.Node] = None
@ -686,34 +695,33 @@ class Bomb(ba.Actor):
self._explode_callbacks: List[Callable[[Bomb, Blast], Any]] = []
# the player this came from
self.source_player = source_player
# The player this came from.
self._source_player = source_player
# by default our hit type/subtype is our own, but we pick up types of
# whoever sets us off so we know what caused a chain reaction
# By default our hit type/subtype is our own, but we pick up types of
# whoever sets us off so we know what caused a chain reaction.
# UPDATE (July 2020): not inheriting hit-types anymore; this causes
# weird effects such as land-mines inheriting 'punch' hit types and
# then not being able to destroy certain things they normally could,
# etc. Inheriting owner/source-node from things that set us off
# should be all we need I think...
self.hit_type = 'explosion'
self.hit_subtype = self.bomb_type
# if no owner was provided, use an unconnected node ref
# (nevermind; trying to use None in these type cases instead)
# if owner is None:
# owner = ba.Node(None)
# the node this came from
# The node this came from.
# FIXME: can we unify this and source_player?
self.owner = owner
# adding footing-materials to things can screw up jumping and flying
# 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?..
# Adding footing-materials to things can screw up jumping and flying
# 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, ...]
if self.bomb_type == 'tnt':
materials = (factory.bomb_material,
ba.sharedobj('footing_material'),
ba.sharedobj('object_material'))
materials = (factory.bomb_material, shared.footing_material,
shared.object_material)
else:
materials = (factory.bomb_material,
ba.sharedobj('object_material'))
materials = (factory.bomb_material, shared.object_material)
if self.bomb_type == 'impact':
materials = materials + (factory.impact_blast_material, )
@ -735,6 +743,7 @@ class Bomb(ba.Actor):
'model': factory.land_mine_model,
'light_model': factory.land_mine_model,
'body': 'landMine',
'body_scale': self.scale,
'shadow_size': 0.44,
'color_texture': factory.land_mine_tex,
'reflection': 'powerup',
@ -752,6 +761,7 @@ class Bomb(ba.Actor):
'model': factory.tnt_model,
'light_model': factory.tnt_model,
'body': 'crate',
'body_scale': self.scale,
'shadow_size': 0.5,
'color_texture': factory.tnt_tex,
'reflection': 'soft',
@ -767,6 +777,7 @@ class Bomb(ba.Actor):
'position': position,
'velocity': velocity,
'body': 'sphere',
'body_scale': self.scale,
'model': factory.impact_bomb_model,
'shadow_size': 0.3,
'color_texture': factory.impact_tex,
@ -804,6 +815,7 @@ class Bomb(ba.Actor):
'position': position,
'velocity': velocity,
'model': model,
'body_scale': self.scale,
'shadow_size': 0.3,
'color_texture': tex,
'sticky': sticky,
@ -828,17 +840,23 @@ class Bomb(ba.Actor):
ba.timer(fuse_time,
ba.WeakCall(self.handlemessage, ExplodeMessage()))
ba.animate(self.node, 'model_scale', {0: 0, 0.2: 1.3, 0.26: 1})
ba.animate(self.node, 'model_scale', {
0: 0,
0.2: 1.3 * self.scale,
0.26: self.scale
})
def get_source_player(self) -> Optional[ba.Player]:
"""Returns a ba.Player representing the source of this bomb.
Be prepared for values of None or invalid Player refs."""
return self.source_player
def get_source_player(
self, playertype: Type[PlayerType]) -> Optional[PlayerType]:
"""Return the source-player if one exists and is the provided type."""
player: Any = self._source_player
return (player if isinstance(player, playertype) and player.exists()
else None)
def on_expire(self) -> None:
super().on_expire()
# release callbacks/refs so we don't wind up with dependency loops..
# Release callbacks/refs so we don't wind up with dependency loops.
self._explode_callbacks = []
def _handle_die(self) -> None:
@ -849,14 +867,14 @@ class Bomb(ba.Actor):
self.handlemessage(ba.DieMessage())
def _handle_impact(self) -> None:
node = ba.get_collision_info('opposing_node')
# if we're an impact bomb and we came from this node, don't explode...
# alternately if we're hitting another impact-bomb from the same
# source, don't explode...
try:
node_delegate = node.getdelegate()
except Exception:
node_delegate = None
node = ba.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).
# Alternately if we're hitting another impact-bomb from the same
# source, don't explode. (can cause accidental explosions if rapidly
# throwing/etc.)
node_delegate = node.getdelegate(object)
if node:
if (self.bomb_type == 'impact' and
(node is self.owner or
@ -870,24 +888,22 @@ class Bomb(ba.Actor):
self.arm_timer = ba.Timer(
1.25, ba.WeakCall(self.handlemessage, ArmMessage()))
# once we've thrown a sticky bomb we can stick to it..
# Once we've thrown a sticky bomb we can stick to it.
elif self.bomb_type == 'sticky':
def _safesetattr(node: Optional[ba.Node], attr: str,
value: Any) -> None:
def _setsticky(node: ba.Node) -> None:
if node:
setattr(node, attr, value)
node.stick_to_owner = True
ba.timer(0.25,
lambda: _safesetattr(self.node, 'stick_to_owner', True))
ba.timer(0.25, lambda: _setsticky(self.node))
def _handle_splat(self) -> None:
node = ba.get_collision_info('opposing_node')
node = ba.getcollision().opposingnode
if (node is not self.owner
and ba.time() - self._last_sticky_sound_time > 1.0):
self._last_sticky_sound_time = ba.time()
assert self.node
ba.playsound(get_factory().sticky_impact_sound,
ba.playsound(BombFactory.get().sticky_impact_sound,
2.0,
position=self.node.position)
@ -903,26 +919,25 @@ class Bomb(ba.Actor):
if self._exploded:
return
self._exploded = True
activity = self.getactivity()
if activity is not None and self.node:
if self.node:
blast = Blast(position=self.node.position,
velocity=self.node.velocity,
blast_radius=self.blast_radius,
blast_type=self.bomb_type,
source_player=self.source_player,
source_player=ba.existing(self._source_player),
hit_type=self.hit_type,
hit_subtype=self.hit_subtype).autoretain()
for callback in self._explode_callbacks:
callback(self, blast)
# we blew up so we need to go away
# FIXME; was there a reason we need this delay?
# 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()))
def _handle_warn(self) -> None:
if self.texture_sequence and self.node:
self.texture_sequence.rate = 30
ba.playsound(get_factory().warn_sound,
ba.playsound(BombFactory.get().warn_sound,
0.5,
position=self.node.position)
@ -941,7 +956,7 @@ class Bomb(ba.Actor):
"""
if not self.node:
return
factory = get_factory()
factory = BombFactory.get()
intex: Sequence[ba.Texture]
if self.bomb_type == 'land_mine':
intex = (factory.land_mine_lit_tex, factory.land_mine_tex)
@ -952,6 +967,7 @@ class Bomb(ba.Actor):
'input_textures': intex
})
ba.timer(0.5, self.texture_sequence.delete)
# We now make it explodable.
ba.timer(
0.25,
@ -978,28 +994,32 @@ class Bomb(ba.Actor):
ba.playsound(factory.activate_sound, 0.5, position=self.node.position)
def _handle_hit(self, msg: ba.HitMessage) -> None:
ispunch = (msg.srcnode and msg.srcnode.getnodetype() == 'spaz')
ispunched = (msg.srcnode and msg.srcnode.getnodetype() == 'spaz')
# Normal bombs are triggered by non-punch impacts;
# impact-bombs by all impacts.
if (not self._exploded and not ispunch
or self.bomb_type in ['impact', 'land_mine']):
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).
if msg.source_player not in [None]:
self.source_player = msg.source_player
source_player = msg.get_source_player(ba.Player)
if source_player is not None:
self._source_player = source_player
# Also inherit the hit type (if a landmine sets off by a bomb,
# the credit should go to the mine)
# the exception is TNT. TNT always gets credit.
if self.bomb_type != 'tnt':
self.hit_type = msg.hit_type
self.hit_subtype = msg.hit_subtype
# UPDATE (July 2020): not doing this anymore. Causes too much
# weird logic such as bombs acting like punches. Holler if
# anything is noticeably broken due to this.
# if self.bomb_type != 'tnt':
# self.hit_type = msg.hit_type
# self.hit_subtype = msg.hit_subtype
ba.timer(100 + int(random.random() * 100),
ba.WeakCall(self.handlemessage, ExplodeMessage()),
timeformat=ba.TimeFormat.MILLISECONDS)
ba.timer(0.1 + random.random() * 0.1,
ba.WeakCall(self.handlemessage, ExplodeMessage()))
assert self.node
self.node.handlemessage('impulse', msg.pos[0], msg.pos[1], msg.pos[2],
msg.velocity[0], msg.velocity[1],
@ -1016,12 +1036,14 @@ class Bomb(ba.Actor):
self.explode()
elif isinstance(msg, ImpactMessage):
self._handle_impact()
elif isinstance(msg, ba.PickedUpMessage):
# change our source to whoever just picked us up *only* if its None
# this way we can get points for killing bots with their own bombs
# hmm would there be a downside to this?...
if self.source_player is not None:
self.source_player = msg.node.source_player
# 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):
# # 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?
# if self._source_player is not None:
# self._source_player = msg.node.source_player
elif isinstance(msg, SplatMessage):
self._handle_splat()
elif isinstance(msg, ba.DroppedMessage):

View File

@ -1,23 +1,5 @@
# Copyright (c) 2011-2020 Eric Froemling
# Released under the MIT License. See LICENSE for details.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Defines Actors related to controls guides."""
from __future__ import annotations
@ -243,6 +225,13 @@ class ControlsGuide(ba.Actor):
# Don't do anything until our delay has passed.
ba.timer(delay, ba.WeakCall(self._start_updating))
@staticmethod
def _meaningful_button_name(device: ba.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
@ -264,7 +253,9 @@ class ControlsGuide(ba.Actor):
# If we have a touchscreen, we only fade in if we have a player with
# an input device that is *not* the touchscreen.
touchscreen: Optional[ba.InputDevice] = _ba.get_input_device(
# (otherwise it is confusing to see the touchscreen buttons right
# next to our display buttons)
touchscreen: Optional[ba.InputDevice] = _ba.getinputdevice(
'TouchScreen', '#1', doraise=False)
if touchscreen is not None:
@ -272,7 +263,7 @@ class ControlsGuide(ba.Actor):
# We want to get ones who are still in the process of
# selecting a character, etc.
input_devices = [
p.get_input_device() for p in ba.getsession().players
p.inputdevice for p in ba.getsession().sessionplayers
]
input_devices = [
i for i in input_devices if i and i is not touchscreen
@ -284,8 +275,8 @@ class ControlsGuide(ba.Actor):
for device in input_devices:
for name in ('buttonPunch', 'buttonJump', 'buttonBomb',
'buttonPickUp'):
if device.get_button_name(
get_device_value(device, name)) != '':
if self._meaningful_button_name(
device, get_device_value(device, name)) != '':
fade_in = True
break
if fade_in:
@ -325,13 +316,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.get_input_device() for p in ba.getsession().players]
input_devices = [p.inputdevice for p in ba.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.get_input_device('Keyboard', '#1', doraise=False)
kbd = _ba.getinputdevice('Keyboard', '#1', doraise=False)
if kbd is not None:
input_devices.append(kbd)
@ -368,20 +359,20 @@ class ControlsGuide(ba.Actor):
# Ignore empty values; things like the remote app or
# wiimotes can return these.
bname = device.get_button_name(
get_device_value(device, 'buttonPunch'))
bname = self._meaningful_button_name(
device, get_device_value(device, 'buttonPunch'))
if bname != '':
punch_button_names.add(bname)
bname = device.get_button_name(
get_device_value(device, 'buttonJump'))
bname = self._meaningful_button_name(
device, get_device_value(device, 'buttonJump'))
if bname != '':
jump_button_names.add(bname)
bname = device.get_button_name(
get_device_value(device, 'buttonBomb'))
bname = self._meaningful_button_name(
device, get_device_value(device, 'buttonBomb'))
if bname != '':
bomb_button_names.add(bname)
bname = device.get_button_name(
get_device_value(device, 'buttonPickUp'))
bname = self._meaningful_button_name(
device, get_device_value(device, 'buttonPickUp'))
if bname != '':
pickup_button_names.add(bname)
@ -470,8 +461,7 @@ class ControlsGuide(ba.Actor):
return not self._dead
def handlemessage(self, msg: Any) -> Any:
if __debug__:
self._handlemessage_sanity_check()
assert not self.expired
if isinstance(msg, ba.DieMessage):
if msg.immediate:
self._die()

Some files were not shown because too many files have changed in this diff Show More