mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-24 07:53:30 +08:00
commit
a6fb047189
38
.editorconfig
Normal file
38
.editorconfig
Normal 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
|
||||
|
||||
2936
.efrocachemap
2936
.efrocachemap
File diff suppressed because it is too large
Load Diff
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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.
|
||||
10
.github/ISSUE_TEMPLATE/want-to-discuss-something-or-ask-a-question-.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/want-to-discuss-something-or-ask-a-question-.md
vendored
Normal 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.
|
||||
35
.github/workflows/ci.yml
vendored
35
.github/workflows/ci.yml
vendored
@ -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
3
.gitignore
vendored
@ -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
|
||||
|
||||
3
.idea/ballisticacore.iml
generated
3
.idea/ballisticacore.iml
generated
@ -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>
|
||||
398
.idea/dictionaries/ericf.xml
generated
398
.idea/dictionaries/ericf.xml
generated
File diff suppressed because it is too large
Load Diff
8
.idea/dictionaries/roman.xml
generated
Normal file
8
.idea/dictionaries/roman.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="roman">
|
||||
<words>
|
||||
<w>maxlen</w>
|
||||
<w>pagename</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
</component>
|
||||
6
.idea/inspectionProfiles/Default.xml
generated
6
.idea/inspectionProfiles/Default.xml
generated
@ -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
2
.idea/misc.xml
generated
@ -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>
|
||||
|
||||
214
CHANGELOG.md
214
CHANGELOG.md
@ -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
|
||||
|
||||
@ -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
|
||||
4
LICENSE
4
LICENSE
@ -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
|
||||
|
||||
@ -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
@ -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"
|
||||
]
|
||||
26298
assets/Makefile
26298
assets/Makefile
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
||||
|
||||
@ -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 = []
|
||||
|
||||
@ -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
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
186
assets/src/ba_data/python/ba/_ads.py
Normal file
186
assets/src/ba_data/python/ba/_ads.py
Normal 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.
|
||||
73
assets/src/ba_data/python/ba/_analytics.py
Normal file
73
assets/src/ba_data/python/ba/_analytics.py
Normal 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()
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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")
|
||||
|
||||
3
assets/src/ba_data/python/ba/_appmode.py
Normal file
3
assets/src/ba_data/python/ba/_appmode.py
Normal file
@ -0,0 +1,3 @@
|
||||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Functionality related to the high level state of the app."""
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
72
assets/src/ba_data/python/ba/_collision.py
Normal file
72
assets/src/ba_data/python/ba/_collision.py
Normal 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
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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}))
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
@ -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:
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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('.', '_')
|
||||
|
||||
@ -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']
|
||||
}
|
||||
|
||||
@ -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__':
|
||||
|
||||
35
assets/src/ba_data/python/ba/_keyboard.py
Normal file
35
assets/src/ba_data/python/ba/_keyboard.py
Normal 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, ...]
|
||||
@ -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
|
||||
562
assets/src/ba_data/python/ba/_language.py
Normal file
562
assets/src/ba_data/python/ba/_language.py
Normal 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()
|
||||
@ -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
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
330
assets/src/ba_data/python/ba/_player.py
Normal file
330
assets/src/ba_data/python/ba/_player.py
Normal 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
|
||||
@ -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}')
|
||||
|
||||
96
assets/src/ba_data/python/ba/_plugin.py
Normal file
96
assets/src/ba_data/python/ba/_plugin.py
Normal 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."""
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
56
assets/src/ba_data/python/ba/_score.py
Normal file
56
assets/src/ba_data/python/ba/_score.py
Normal 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 = ''
|
||||
@ -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
84
assets/src/ba_data/python/ba/_settings.py
Normal file
84
assets/src/ba_data/python/ba/_settings.py
Normal 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]]
|
||||
@ -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')
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
"""
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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
|
||||
|
||||
167
assets/src/ba_data/python/ba/_ui.py
Normal file
167
assets/src/ba_data/python/ba/_ui.py
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.')
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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, )),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user