mirror of
https://github.com/imayushsaini/Bombsquad-Ballistica-Modded-Server.git
synced 2026-08-15 13:04:30 +00:00
Merge branch '1.7' into public-server
This commit is contained in:
commit
b29dbb2f2b
283 changed files with 35136 additions and 23221 deletions
|
|
@ -1,901 +0,0 @@
|
|||
#!/usr/bin/env -S python3.10 -O
|
||||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""BallisticaCore server manager."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from threading import Lock, Thread, current_thread
|
||||
from typing import TYPE_CHECKING
|
||||
from nbstreamreader import NonBlockingStreamReader as NBSR
|
||||
import _thread
|
||||
ERROR_LOGGING=False
|
||||
|
||||
# We make use of the bacommon and efro packages as well as site-packages
|
||||
# included with our bundled Ballistica dist, so we need to add those paths
|
||||
# before we import them.
|
||||
sys.path += [
|
||||
str(Path(Path(__file__).parent, 'dist', 'ba_data', 'python')),
|
||||
str(Path(Path(__file__).parent, 'dist', 'ba_data', 'python-site-packages'))
|
||||
]
|
||||
|
||||
from bacommon.servermanager import ServerConfig, StartServerModeCommand
|
||||
from efro.dataclassio import dataclass_from_dict, dataclass_validate
|
||||
from efro.error import CleanError
|
||||
from efro.terminal import Clr
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from types import FrameType
|
||||
from bacommon.servermanager import ServerCommand
|
||||
|
||||
VERSION_STR = '1.3'
|
||||
|
||||
# Version history:
|
||||
# 1.3.1
|
||||
# Windows binary is now named BallisticaCoreHeadless.exe
|
||||
# 1.3:
|
||||
# Added show_tutorial config option
|
||||
# Added team_names config option
|
||||
# Added team_colors config option
|
||||
# Added playlist_inline config option
|
||||
# 1.2:
|
||||
# Added optional --help arg
|
||||
# Added --config arg for specifying config file and --root for ba_root path
|
||||
# Added noninteractive mode and --interactive/--noninteractive args to
|
||||
# explicitly enable/disable it (it is autodetected by default)
|
||||
# Added explicit control for auto-restart: --no-auto-restart
|
||||
# Config file is now reloaded each time server binary is restarted; no more
|
||||
# need to bring down server wrapper to pick up changes
|
||||
# Now automatically restarts server binary when config file is modified
|
||||
# (use --no-config-auto-restart to disable that behavior)
|
||||
# 1.1.1:
|
||||
# Switched config reading to use efro.dataclasses.dataclass_from_dict()
|
||||
# 1.1.0:
|
||||
# Added shutdown command
|
||||
# Changed restart to default to immediate=True
|
||||
# Added clean_exit_minutes, unclean_exit_minutes, and idle_exit_minutes
|
||||
# 1.0.0:
|
||||
# Initial release
|
||||
|
||||
|
||||
class ServerManagerApp:
|
||||
"""An app which manages BallisticaCore server execution.
|
||||
|
||||
Handles configuring, launching, re-launching, and otherwise
|
||||
managing BallisticaCore operating in server mode.
|
||||
"""
|
||||
|
||||
# How many seconds we wait after asking our subprocess to do an immediate
|
||||
# shutdown before bringing down the hammer.
|
||||
IMMEDIATE_SHUTDOWN_TIME_LIMIT = 5.0
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._config_path = 'config.yaml'
|
||||
self._user_provided_config_path = False
|
||||
self._config = ServerConfig()
|
||||
self._ba_root_path = os.path.abspath('dist/ba_root')
|
||||
self._interactive = sys.stdin.isatty()
|
||||
self._wrapper_shutdown_desired = False
|
||||
self._done = False
|
||||
self._subprocess_commands: list[str | ServerCommand] = []
|
||||
self._subprocess_commands_lock = Lock()
|
||||
self._subprocess_force_kill_time: float | None = None
|
||||
self._auto_restart = True
|
||||
self._config_auto_restart = True
|
||||
self._config_mtime: float | None = None
|
||||
self._last_config_mtime_check_time: float | None = None
|
||||
self._should_report_subprocess_error = False
|
||||
self._running = False
|
||||
self._interpreter_start_time: float | None = None
|
||||
self._subprocess: subprocess.Popen[bytes] | None = None
|
||||
self._subprocess_launch_time: float | None = None
|
||||
self._subprocess_sent_config_auto_restart = False
|
||||
self._subprocess_sent_clean_exit = False
|
||||
self._subprocess_sent_unclean_exit = False
|
||||
self._subprocess_thread: Thread | None = None
|
||||
self._subprocess_exited_cleanly: bool | None = None
|
||||
self.nbsr = None
|
||||
|
||||
# This may override the above defaults.
|
||||
self._parse_command_line_args()
|
||||
|
||||
# Do an initial config-load. If the config is invalid at this point
|
||||
# we can cleanly die (we're more lenient later on reloads).
|
||||
self.load_config(strict=True, print_confirmation=False)
|
||||
|
||||
@property
|
||||
def config(self) -> ServerConfig:
|
||||
"""The current config for the app."""
|
||||
return self._config
|
||||
|
||||
@config.setter
|
||||
def config(self, value: ServerConfig) -> None:
|
||||
dataclass_validate(value)
|
||||
self._config = value
|
||||
|
||||
def _prerun(self) -> None:
|
||||
"""Common code at the start of any run."""
|
||||
|
||||
# Make sure we don't call run multiple times.
|
||||
if self._running:
|
||||
raise RuntimeError('Already running.')
|
||||
self._running = True
|
||||
|
||||
dbgstr = 'debug' if __debug__ else 'opt'
|
||||
print(
|
||||
f'{Clr.CYN}{Clr.BLD}BallisticaCore server manager {VERSION_STR}'
|
||||
f' starting up ({dbgstr} mode)...{Clr.RST}',
|
||||
flush=True)
|
||||
|
||||
# Python will handle SIGINT for us (as KeyboardInterrupt) but we
|
||||
# need to register a SIGTERM handler so we have a chance to clean
|
||||
# up our subprocess when someone tells us to die. (and avoid
|
||||
# zombie processes)
|
||||
signal.signal(signal.SIGTERM, self._handle_term_signal)
|
||||
|
||||
# During a run, we make the assumption that cwd is the dir
|
||||
# containing this script, so make that so. Up until now that may
|
||||
# not be the case (we support being called from any location).
|
||||
os.chdir(os.path.abspath(os.path.dirname(__file__)))
|
||||
|
||||
# Fire off a background thread to wrangle our server binaries.
|
||||
self._subprocess_thread = Thread(target=self._bg_thread_main)
|
||||
self._subprocess_thread.start()
|
||||
|
||||
def _postrun(self) -> None:
|
||||
"""Common code at the end of any run."""
|
||||
print(f'{Clr.CYN}Server manager shutting down...{Clr.RST}', flush=True)
|
||||
|
||||
assert self._subprocess_thread is not None
|
||||
if self._subprocess_thread.is_alive():
|
||||
print(f'{Clr.CYN}Waiting for subprocess exit...{Clr.RST}',
|
||||
flush=True)
|
||||
|
||||
# Mark ourselves as shutting down and wait for the process to wrap up.
|
||||
self._done = True
|
||||
self._subprocess_thread.join()
|
||||
|
||||
# If there's a server error we should care about, exit the
|
||||
# entire wrapper uncleanly.
|
||||
if self._should_report_subprocess_error:
|
||||
raise CleanError('Server subprocess exited uncleanly.')
|
||||
|
||||
def run(self) -> None:
|
||||
"""Do the thing."""
|
||||
if self._interactive:
|
||||
self._run_interactive()
|
||||
else:
|
||||
self._run_noninteractive()
|
||||
|
||||
def _run_noninteractive(self) -> None:
|
||||
"""Run the app loop to completion noninteractively."""
|
||||
self._prerun()
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1.234)
|
||||
except KeyboardInterrupt:
|
||||
# Gracefully bow out if we kill ourself via keyboard.
|
||||
pass
|
||||
except SystemExit:
|
||||
# We get this from the builtin quit(), our signal handler, etc.
|
||||
# Need to catch this so we can clean up, otherwise we'll be
|
||||
# left in limbo with our process thread still running.
|
||||
pass
|
||||
self._postrun()
|
||||
|
||||
def _run_interactive(self) -> None:
|
||||
"""Run the app loop to completion interactively."""
|
||||
import code
|
||||
self._prerun()
|
||||
|
||||
# Print basic usage info for interactive mode.
|
||||
print(
|
||||
f"{Clr.CYN}Interactive mode enabled; use the 'mgr' object"
|
||||
f' to interact with the server.\n'
|
||||
f"Type 'help(mgr)' for more information.{Clr.RST}",
|
||||
flush=True)
|
||||
|
||||
context = {'__name__': '__console__', '__doc__': None, 'mgr': self}
|
||||
|
||||
# Enable tab-completion if possible.
|
||||
self._enable_tab_completion(context)
|
||||
|
||||
# Now just sit in an interpreter.
|
||||
# TODO: make it possible to use IPython if the user has it available.
|
||||
try:
|
||||
self._interpreter_start_time = time.time()
|
||||
code.interact(local=context, banner='', exitmsg='')
|
||||
except SystemExit:
|
||||
# We get this from the builtin quit(), our signal handler, etc.
|
||||
# Need to catch this so we can clean up, otherwise we'll be
|
||||
# left in limbo with our process thread still running.
|
||||
pass
|
||||
except BaseException as exc:
|
||||
print(
|
||||
f'{Clr.SRED}Unexpected interpreter exception:'
|
||||
f' {exc} ({type(exc)}){Clr.RST}',
|
||||
flush=True)
|
||||
|
||||
self._postrun()
|
||||
|
||||
def cmd(self, statement: str) -> None:
|
||||
"""Exec a Python command on the current running server subprocess.
|
||||
|
||||
Note that commands are executed asynchronously and no status or
|
||||
return value is accessible from this manager app.
|
||||
"""
|
||||
if not isinstance(statement, str):
|
||||
raise TypeError(f'Expected a string arg; got {type(statement)}')
|
||||
with self._subprocess_commands_lock:
|
||||
self._subprocess_commands.append(statement)
|
||||
self._block_for_command_completion()
|
||||
|
||||
def _block_for_command_completion(self) -> None:
|
||||
# Ideally we'd block here until the command was run so our prompt would
|
||||
# print after it's results. We currently don't get any response from
|
||||
# the app so the best we can do is block until our bg thread has sent
|
||||
# it. In the future we can perhaps add a proper 'command port'
|
||||
# interface for proper blocking two way communication.
|
||||
while True:
|
||||
with self._subprocess_commands_lock:
|
||||
if not self._subprocess_commands:
|
||||
break
|
||||
time.sleep(0.1)
|
||||
|
||||
# One last short delay so if we come out *just* as the command is sent
|
||||
# we'll hopefully still give it enough time to process/print.
|
||||
time.sleep(0.1)
|
||||
|
||||
def screenmessage(self,
|
||||
message: str,
|
||||
color: tuple[float, float, float] | None = None,
|
||||
clients: list[int] | None = None) -> None:
|
||||
"""Display a screen-message.
|
||||
|
||||
This will have no name attached and not show up in chat history.
|
||||
They will show up in replays, however (unless clients is passed).
|
||||
"""
|
||||
from bacommon.servermanager import ScreenMessageCommand
|
||||
self._enqueue_server_command(
|
||||
ScreenMessageCommand(message=message, color=color,
|
||||
clients=clients))
|
||||
|
||||
def chatmessage(self,
|
||||
message: str,
|
||||
clients: list[int] | None = None) -> None:
|
||||
"""Send a chat message from the server.
|
||||
|
||||
This will have the server's name attached and will be logged
|
||||
in client chat windows, just like other chat messages.
|
||||
"""
|
||||
from bacommon.servermanager import ChatMessageCommand
|
||||
self._enqueue_server_command(
|
||||
ChatMessageCommand(message=message, clients=clients))
|
||||
|
||||
def clientlist(self) -> None:
|
||||
"""Print a list of connected clients."""
|
||||
from bacommon.servermanager import ClientListCommand
|
||||
self._enqueue_server_command(ClientListCommand())
|
||||
self._block_for_command_completion()
|
||||
|
||||
def kick(self, client_id: int, ban_time: int | None = None) -> None:
|
||||
"""Kick the client with the provided id.
|
||||
|
||||
If ban_time is provided, the client will be banned for that
|
||||
length of time in seconds. If it is None, ban duration will
|
||||
be determined automatically. Pass 0 or a negative number for no
|
||||
ban time.
|
||||
"""
|
||||
from bacommon.servermanager import KickCommand
|
||||
self._enqueue_server_command(
|
||||
KickCommand(client_id=client_id, ban_time=ban_time))
|
||||
|
||||
def restart(self, immediate: bool = True) -> None:
|
||||
"""Restart the server subprocess.
|
||||
|
||||
By default, the current server process will exit immediately.
|
||||
If 'immediate' is passed as False, however, it will instead exit at
|
||||
the next clean transition point (the end of a series, etc).
|
||||
"""
|
||||
from bacommon.servermanager import ShutdownCommand, ShutdownReason
|
||||
self._enqueue_server_command(
|
||||
ShutdownCommand(reason=ShutdownReason.RESTARTING,
|
||||
immediate=immediate))
|
||||
|
||||
# If we're asking for an immediate restart but don't get one within
|
||||
# the grace period, bring down the hammer.
|
||||
if immediate:
|
||||
self._subprocess_force_kill_time = (
|
||||
time.time() + self.IMMEDIATE_SHUTDOWN_TIME_LIMIT)
|
||||
|
||||
def shutdown(self, immediate: bool = True) -> None:
|
||||
"""Shut down the server subprocess and exit the wrapper.
|
||||
|
||||
By default, the current server process will exit immediately.
|
||||
If 'immediate' is passed as False, however, it will instead exit at
|
||||
the next clean transition point (the end of a series, etc).
|
||||
"""
|
||||
from bacommon.servermanager import ShutdownCommand, ShutdownReason
|
||||
self._enqueue_server_command(
|
||||
ShutdownCommand(reason=ShutdownReason.NONE, immediate=immediate))
|
||||
|
||||
# An explicit shutdown means we know to bail completely once this
|
||||
# subprocess completes.
|
||||
self._wrapper_shutdown_desired = True
|
||||
|
||||
# If we're asking for an immediate shutdown but don't get one within
|
||||
# the grace period, bring down the hammer.
|
||||
if immediate:
|
||||
self._subprocess_force_kill_time = (
|
||||
time.time() + self.IMMEDIATE_SHUTDOWN_TIME_LIMIT)
|
||||
|
||||
def _parse_command_line_args(self) -> None:
|
||||
"""Parse command line args."""
|
||||
# pylint: disable=too-many-branches
|
||||
|
||||
i = 1
|
||||
argc = len(sys.argv)
|
||||
did_set_interactive = False
|
||||
while i < argc:
|
||||
arg = sys.argv[i]
|
||||
if arg == '--help':
|
||||
self.print_help()
|
||||
sys.exit(0)
|
||||
elif arg == '--config':
|
||||
if i + 1 >= argc:
|
||||
raise CleanError('Expected a config path as next arg.')
|
||||
path = sys.argv[i + 1]
|
||||
if not os.path.exists(path):
|
||||
raise CleanError(
|
||||
f"Supplied path does not exist: '{path}'.")
|
||||
# We need an abs path because we may be in a different
|
||||
# cwd currently than we will be during the run.
|
||||
self._config_path = os.path.abspath(path)
|
||||
self._user_provided_config_path = True
|
||||
i += 2
|
||||
elif arg == '--root':
|
||||
if i + 1 >= argc:
|
||||
raise CleanError('Expected a path as next arg.')
|
||||
path = sys.argv[i + 1]
|
||||
# Unlike config_path, this one doesn't have to exist now.
|
||||
# We do however need an abs path because we may be in a
|
||||
# different cwd currently than we will be during the run.
|
||||
self._ba_root_path = os.path.abspath(path)
|
||||
i += 2
|
||||
elif arg == '--interactive':
|
||||
if did_set_interactive:
|
||||
raise CleanError('interactive/noninteractive can only'
|
||||
' be specified once.')
|
||||
self._interactive = True
|
||||
did_set_interactive = True
|
||||
i += 1
|
||||
elif arg == '--noninteractive':
|
||||
if did_set_interactive:
|
||||
raise CleanError('interactive/noninteractive can only'
|
||||
' be specified once.')
|
||||
self._interactive = False
|
||||
did_set_interactive = True
|
||||
i += 1
|
||||
elif arg == '--no-auto-restart':
|
||||
self._auto_restart = False
|
||||
i += 1
|
||||
elif arg == '--no-config-auto-restart':
|
||||
self._config_auto_restart = False
|
||||
i += 1
|
||||
else:
|
||||
raise CleanError(f"Invalid arg: '{arg}'.")
|
||||
|
||||
@classmethod
|
||||
def _par(cls, txt: str) -> str:
|
||||
"""Spit out a pretty paragraph for our help text."""
|
||||
import textwrap
|
||||
ind = ' ' * 2
|
||||
out = textwrap.fill(txt, 80, initial_indent=ind, subsequent_indent=ind)
|
||||
return f'{out}\n'
|
||||
|
||||
@classmethod
|
||||
def print_help(cls) -> None:
|
||||
"""Print app help."""
|
||||
filename = os.path.basename(__file__)
|
||||
out = (
|
||||
f'{Clr.BLD}{filename} usage:{Clr.RST}\n' + cls._par(
|
||||
'This script handles configuring, launching, re-launching,'
|
||||
' and otherwise managing BallisticaCore operating'
|
||||
' in server mode. It can be run with no arguments, but'
|
||||
' accepts the following optional ones:') + f'\n'
|
||||
f'{Clr.BLD}--help:{Clr.RST}\n'
|
||||
f' Show this help.\n'
|
||||
f'\n'
|
||||
f'{Clr.BLD}--config [path]{Clr.RST}\n' + cls._par(
|
||||
'Set the config file read by the server script. The config'
|
||||
' file contains most options for what kind of game to host.'
|
||||
' It should be in yaml format. Note that yaml is backwards'
|
||||
' compatible with json so you can just write json if you'
|
||||
' want to. If not specified, the script will look for a'
|
||||
' file named \'config.yaml\' in the same directory as the'
|
||||
' script.') + '\n'
|
||||
f'{Clr.BLD}--root [path]{Clr.RST}\n' + cls._par(
|
||||
'Set the ballistica root directory. This is where the server'
|
||||
' binary will read and write its caches, state files,'
|
||||
' downloaded assets to, etc. It needs to be a writable'
|
||||
' directory. If not specified, the script will use the'
|
||||
' \'dist/ba_root\' directory relative to itself.') + '\n'
|
||||
f'{Clr.BLD}--interactive{Clr.RST}\n'
|
||||
f'{Clr.BLD}--noninteractive{Clr.RST}\n' + cls._par(
|
||||
'Specify whether the script should run interactively.'
|
||||
' In interactive mode, the script creates a Python interpreter'
|
||||
' and reads commands from stdin, allowing for live interaction'
|
||||
' with the server. The server script will then exit when '
|
||||
'end-of-file is reached in stdin. Noninteractive mode creates'
|
||||
' no interpreter and is more suited to being run in automated'
|
||||
' scenarios. By default, interactive mode will be used if'
|
||||
' a terminal is detected and noninteractive mode otherwise.') +
|
||||
'\n'
|
||||
f'{Clr.BLD}--no-auto-restart{Clr.RST}\n' +
|
||||
cls._par('Auto-restart is enabled by default, which means the'
|
||||
' server manager will restart the server binary whenever'
|
||||
' it exits (even when uncleanly). Disabling auto-restart'
|
||||
' will cause the server manager to instead exit after a'
|
||||
' single run and also to return error codes if the'
|
||||
' server binary did so.') + '\n'
|
||||
f'{Clr.BLD}--no-config-auto-restart{Clr.RST}\n' + cls._par(
|
||||
'By default, when auto-restart is enabled, the server binary'
|
||||
' will be automatically restarted if changes to the server'
|
||||
' config file are detected. This disables that behavior.'))
|
||||
print(out)
|
||||
|
||||
def load_config(self, strict: bool, print_confirmation: bool) -> None:
|
||||
"""Load the config.
|
||||
|
||||
If strict is True, errors will propagate upward.
|
||||
Otherwise, warnings will be printed and repeated attempts will be
|
||||
made to load the config. Eventually the function will give up
|
||||
and leave the existing config as-is.
|
||||
"""
|
||||
retry_seconds = 3
|
||||
maxtries = 11
|
||||
for trynum in range(maxtries):
|
||||
try:
|
||||
self._config = self._load_config_from_file(
|
||||
print_confirmation=print_confirmation)
|
||||
return
|
||||
except Exception as exc:
|
||||
if strict:
|
||||
raise CleanError(
|
||||
f'Error loading config file:\n{exc}') from exc
|
||||
print(f'{Clr.RED}Error loading config file:\n{exc}.{Clr.RST}',
|
||||
flush=True)
|
||||
if trynum == maxtries - 1:
|
||||
print(
|
||||
f'{Clr.RED}Max-tries reached; giving up.'
|
||||
f' Existing config values will be used.{Clr.RST}',
|
||||
flush=True)
|
||||
break
|
||||
print(
|
||||
f'{Clr.CYN}Please correct the error.'
|
||||
f' Will re-attempt load in {retry_seconds}'
|
||||
f' seconds. (attempt {trynum+1} of'
|
||||
f' {maxtries-1}).{Clr.RST}',
|
||||
flush=True)
|
||||
|
||||
for _j in range(retry_seconds):
|
||||
# If the app is trying to die, drop what we're doing.
|
||||
if self._done:
|
||||
return
|
||||
time.sleep(1)
|
||||
|
||||
def _load_config_from_file(self, print_confirmation: bool) -> ServerConfig:
|
||||
|
||||
out: ServerConfig | None = None
|
||||
|
||||
if not os.path.exists(self._config_path):
|
||||
|
||||
# Special case:
|
||||
# If the user didn't specify a particular config file, allow
|
||||
# gracefully falling back to defaults if the default one is
|
||||
# missing.
|
||||
if not self._user_provided_config_path:
|
||||
if print_confirmation:
|
||||
print(
|
||||
f'{Clr.YLW}Default config file not found'
|
||||
f' (\'{self._config_path}\'); using default'
|
||||
f' settings.{Clr.RST}',
|
||||
flush=True)
|
||||
self._config_mtime = None
|
||||
self._last_config_mtime_check_time = time.time()
|
||||
return ServerConfig()
|
||||
|
||||
# Don't be so lenient if the user pointed us at one though.
|
||||
raise RuntimeError(
|
||||
f"Config file not found: '{self._config_path}'.")
|
||||
|
||||
import yaml
|
||||
with open(self._config_path, encoding='utf-8') as infile:
|
||||
user_config_raw = yaml.safe_load(infile.read())
|
||||
|
||||
# An empty config file will yield None, and that's ok.
|
||||
if user_config_raw is not None:
|
||||
out = dataclass_from_dict(ServerConfig, user_config_raw)
|
||||
|
||||
# Update our known mod-time since we know it exists.
|
||||
self._config_mtime = Path(self._config_path).stat().st_mtime
|
||||
self._last_config_mtime_check_time = time.time()
|
||||
|
||||
# Go with defaults if we weren't able to load anything.
|
||||
if out is None:
|
||||
out = ServerConfig()
|
||||
|
||||
if print_confirmation:
|
||||
print(f'{Clr.CYN}Valid server config file loaded.{Clr.RST}',
|
||||
flush=True)
|
||||
return out
|
||||
|
||||
def _enable_tab_completion(self, locs: dict) -> None:
|
||||
"""Enable tab-completion on platforms where available (linux/mac)."""
|
||||
try:
|
||||
import readline
|
||||
import rlcompleter
|
||||
readline.set_completer(rlcompleter.Completer(locs).complete)
|
||||
readline.parse_and_bind('tab:complete')
|
||||
except ImportError:
|
||||
# This is expected (readline doesn't exist under windows).
|
||||
pass
|
||||
|
||||
def _bg_thread_main(self) -> None:
|
||||
"""Top level method run by our bg thread."""
|
||||
while not self._done:
|
||||
self._run_server_cycle()
|
||||
|
||||
def _handle_term_signal(self, sig: int, frame: FrameType | None) -> None:
|
||||
"""Handle signals (will always run in the main thread)."""
|
||||
del sig, frame # Unused.
|
||||
sys.exit(1 if self._should_report_subprocess_error else 0)
|
||||
|
||||
def _run_server_cycle(self) -> None:
|
||||
"""Spin up the server subprocess and run it until exit."""
|
||||
# pylint: disable=consider-using-with
|
||||
|
||||
# Reload our config, and update our overall behavior based on it.
|
||||
# We do non-strict this time to give the user repeated attempts if
|
||||
# if they mess up while modifying the config on the fly.
|
||||
self.load_config(strict=False, print_confirmation=True)
|
||||
|
||||
self._prep_subprocess_environment()
|
||||
|
||||
# Launch the binary and grab its stdin;
|
||||
# we'll use this to feed it commands.
|
||||
self._subprocess_launch_time = time.time()
|
||||
|
||||
# Set an environment var so the server process knows its being
|
||||
# run under us. This causes it to ignore ctrl-c presses and other
|
||||
# slight behavior tweaks. Hmm; should this be an argument instead?
|
||||
os.environ['BA_SERVER_WRAPPER_MANAGED'] = '1'
|
||||
|
||||
print(f'{Clr.CYN}Launching server subprocess...{Clr.RST}', flush=True)
|
||||
binary_name = ('BallisticaCoreHeadless.exe'
|
||||
if os.name == 'nt' else './bombsquad_headless')
|
||||
assert self._ba_root_path is not None
|
||||
self._subprocess = None
|
||||
|
||||
# Launch!
|
||||
try:
|
||||
if ERROR_LOGGING:
|
||||
self._subprocess = subprocess.Popen(
|
||||
[binary_name, '-cfgdir', self._ba_root_path],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
cwd='dist')
|
||||
|
||||
self.nbsr = NBSR(self._subprocess.stdout)
|
||||
self.nbsrerr = NBSR(self._subprocess.stderr)
|
||||
else:
|
||||
self._subprocess = subprocess.Popen(
|
||||
[binary_name, '-cfgdir', self._ba_root_path],
|
||||
stdin=subprocess.PIPE,
|
||||
cwd='dist')
|
||||
except Exception as exc:
|
||||
self._subprocess_exited_cleanly = False
|
||||
print(
|
||||
f'{Clr.RED}Error launching server subprocess: {exc}{Clr.RST}',
|
||||
flush=True)
|
||||
|
||||
# Do the thing.
|
||||
try:
|
||||
self._run_subprocess_until_exit()
|
||||
except Exception as exc:
|
||||
print(f'{Clr.RED}Error running server subprocess: {exc}{Clr.RST}',
|
||||
flush=True)
|
||||
|
||||
self._kill_subprocess()
|
||||
|
||||
assert self._subprocess_exited_cleanly is not None
|
||||
|
||||
# EW: it seems that if we die before the main thread has fully started
|
||||
# up the interpreter, its possible that it will not break out of its
|
||||
# loop via the usual SystemExit that gets sent when we die.
|
||||
if self._interactive:
|
||||
while (self._interpreter_start_time is None
|
||||
or time.time() - self._interpreter_start_time < 0.5):
|
||||
time.sleep(0.1)
|
||||
|
||||
# Avoid super fast death loops.
|
||||
if (not self._subprocess_exited_cleanly and self._auto_restart
|
||||
and not self._done):
|
||||
time.sleep(5.0)
|
||||
|
||||
# If they don't want auto-restart, we'll exit the whole wrapper.
|
||||
# (and with an error code if things ended badly).
|
||||
if not self._auto_restart:
|
||||
self._wrapper_shutdown_desired = True
|
||||
if not self._subprocess_exited_cleanly:
|
||||
self._should_report_subprocess_error = True
|
||||
|
||||
self._reset_subprocess_vars()
|
||||
|
||||
# If we want to die completely after this subprocess has ended,
|
||||
# tell the main thread to die.
|
||||
if self._wrapper_shutdown_desired:
|
||||
|
||||
# Only do this if the main thread is not already waiting for
|
||||
# us to die; otherwise it can lead to deadlock.
|
||||
# (we hang in os.kill while main thread is blocked in Thread.join)
|
||||
if not self._done:
|
||||
self._done = True
|
||||
|
||||
# This should break the main thread out of its blocking
|
||||
# interpreter call.
|
||||
os.kill(os.getpid(), signal.SIGTERM)
|
||||
|
||||
def _prep_subprocess_environment(self) -> None:
|
||||
"""Write files that must exist at process launch."""
|
||||
|
||||
assert self._ba_root_path is not None
|
||||
os.makedirs(self._ba_root_path, exist_ok=True)
|
||||
cfgpath = os.path.join(self._ba_root_path, 'config.json')
|
||||
if os.path.exists(cfgpath):
|
||||
with open(cfgpath, encoding='utf-8') as infile:
|
||||
bincfg = json.loads(infile.read())
|
||||
else:
|
||||
bincfg = {}
|
||||
|
||||
# Some of our config values translate directly into the
|
||||
# ballisticacore config file; the rest we pass at runtime.
|
||||
bincfg['Port'] = self._config.port
|
||||
bincfg['Auto Balance Teams'] = self._config.auto_balance_teams
|
||||
bincfg['Show Tutorial'] = self._config.show_tutorial
|
||||
|
||||
if self._config.team_names is not None:
|
||||
bincfg['Custom Team Names'] = self._config.team_names
|
||||
elif 'Custom Team Names' in bincfg:
|
||||
del bincfg['Custom Team Names']
|
||||
|
||||
if self._config.team_colors is not None:
|
||||
bincfg['Custom Team Colors'] = self._config.team_colors
|
||||
elif 'Custom Team Colors' in bincfg:
|
||||
del bincfg['Custom Team Colors']
|
||||
|
||||
bincfg['Idle Exit Minutes'] = self._config.idle_exit_minutes
|
||||
with open(cfgpath, 'w', encoding='utf-8') as outfile:
|
||||
outfile.write(json.dumps(bincfg))
|
||||
|
||||
def _enqueue_server_command(self, command: ServerCommand) -> None:
|
||||
"""Enqueue a command to be sent to the server.
|
||||
|
||||
Can be called from any thread.
|
||||
"""
|
||||
with self._subprocess_commands_lock:
|
||||
self._subprocess_commands.append(command)
|
||||
|
||||
def _send_server_command(self, command: ServerCommand) -> None:
|
||||
"""Send a command to the server.
|
||||
|
||||
Must be called from the server process thread.
|
||||
"""
|
||||
import pickle
|
||||
assert current_thread() is self._subprocess_thread
|
||||
assert self._subprocess is not None
|
||||
assert self._subprocess.stdin is not None
|
||||
val = repr(pickle.dumps(command))
|
||||
assert '\n' not in val
|
||||
execcode = (f'import ba._servermode;'
|
||||
f' ba._servermode._cmd({val})\n').encode()
|
||||
self._subprocess.stdin.write(execcode)
|
||||
self._subprocess.stdin.flush()
|
||||
|
||||
def _run_subprocess_until_exit(self) -> None:
|
||||
if self._subprocess is None:
|
||||
return
|
||||
|
||||
assert current_thread() is self._subprocess_thread
|
||||
assert self._subprocess.stdin is not None
|
||||
|
||||
# Send the initial server config which should kick things off.
|
||||
# (but make sure its values are still valid first)
|
||||
dataclass_validate(self._config)
|
||||
self._send_server_command(StartServerModeCommand(self._config))
|
||||
|
||||
while True:
|
||||
|
||||
# If the app is trying to shut down, nope out immediately.
|
||||
if self._done:
|
||||
break
|
||||
if ERROR_LOGGING:
|
||||
out = self.nbsr.readline(0.1)
|
||||
out2 = self.nbsrerr.readline(0.1)
|
||||
if out:
|
||||
sys.stdout.write(out.decode("utf-8"))
|
||||
_thread.start_new_thread(dump_logs, (out.decode("utf-8"),))
|
||||
if out2:
|
||||
sys.stdout.write(out2.decode("utf-8"))
|
||||
_thread.start_new_thread(dump_logs, (out2.decode("utf-8"),))
|
||||
# Pass along any commands to our process.
|
||||
with self._subprocess_commands_lock:
|
||||
for incmd in self._subprocess_commands:
|
||||
# If we're passing a raw string to exec, no need to wrap it
|
||||
# in any proper structure.
|
||||
if isinstance(incmd, str):
|
||||
self._subprocess.stdin.write((incmd + '\n').encode())
|
||||
self._subprocess.stdin.flush()
|
||||
else:
|
||||
self._send_server_command(incmd)
|
||||
self._subprocess_commands = []
|
||||
|
||||
# Request restarts/shut-downs for various reasons.
|
||||
self._request_shutdowns_or_restarts()
|
||||
|
||||
# If they want to force-kill our subprocess, simply exit this
|
||||
# loop; the cleanup code will kill the process if its still
|
||||
# alive.
|
||||
if (self._subprocess_force_kill_time is not None
|
||||
and time.time() > self._subprocess_force_kill_time):
|
||||
print(
|
||||
f'{Clr.CYN}Immediate shutdown time limit'
|
||||
f' ({self.IMMEDIATE_SHUTDOWN_TIME_LIMIT:.1f} seconds)'
|
||||
f' expired; force-killing subprocess...{Clr.RST}',
|
||||
flush=True)
|
||||
break
|
||||
|
||||
# Watch for the server process exiting..
|
||||
code: int | None = self._subprocess.poll()
|
||||
if code is not None:
|
||||
|
||||
clr = Clr.CYN if code == 0 else Clr.RED
|
||||
print(
|
||||
f'{clr}Server subprocess exited'
|
||||
f' with code {code}.{Clr.RST}',
|
||||
flush=True)
|
||||
self._subprocess_exited_cleanly = (code == 0)
|
||||
break
|
||||
|
||||
time.sleep(0.25)
|
||||
|
||||
def _request_shutdowns_or_restarts(self) -> None:
|
||||
# pylint: disable=too-many-branches
|
||||
assert current_thread() is self._subprocess_thread
|
||||
assert self._subprocess_launch_time is not None
|
||||
now = time.time()
|
||||
minutes_since_launch = (now - self._subprocess_launch_time) / 60.0
|
||||
|
||||
# If we're doing auto-restart with config changes, handle that.
|
||||
if (self._auto_restart and self._config_auto_restart
|
||||
and not self._subprocess_sent_config_auto_restart):
|
||||
if (self._last_config_mtime_check_time is None
|
||||
or (now - self._last_config_mtime_check_time) > 3.123):
|
||||
self._last_config_mtime_check_time = now
|
||||
mtime: float | None
|
||||
if os.path.isfile(self._config_path):
|
||||
mtime = Path(self._config_path).stat().st_mtime
|
||||
else:
|
||||
mtime = None
|
||||
if mtime != self._config_mtime:
|
||||
print(
|
||||
f'{Clr.CYN}Config-file change detected;'
|
||||
f' requesting immediate restart.{Clr.RST}',
|
||||
flush=True)
|
||||
self.restart(immediate=True)
|
||||
self._subprocess_sent_config_auto_restart = True
|
||||
|
||||
# Attempt clean exit if our clean-exit-time passes.
|
||||
# (and enforce a 6 hour max if not provided)
|
||||
clean_exit_minutes = 360.0
|
||||
if self._config.clean_exit_minutes is not None:
|
||||
clean_exit_minutes = min(clean_exit_minutes,
|
||||
self._config.clean_exit_minutes)
|
||||
if clean_exit_minutes is not None:
|
||||
if (minutes_since_launch > clean_exit_minutes
|
||||
and not self._subprocess_sent_clean_exit):
|
||||
opname = 'restart' if self._auto_restart else 'shutdown'
|
||||
print(
|
||||
f'{Clr.CYN}clean_exit_minutes'
|
||||
f' ({clean_exit_minutes})'
|
||||
f' elapsed; requesting soft'
|
||||
f' {opname}.{Clr.RST}',
|
||||
flush=True)
|
||||
if self._auto_restart:
|
||||
self.restart(immediate=False)
|
||||
else:
|
||||
self.shutdown(immediate=False)
|
||||
self._subprocess_sent_clean_exit = True
|
||||
|
||||
# Attempt unclean exit if our unclean-exit-time passes.
|
||||
# (and enforce a 7 hour max if not provided)
|
||||
unclean_exit_minutes = 420.0
|
||||
if self._config.unclean_exit_minutes is not None:
|
||||
unclean_exit_minutes = min(unclean_exit_minutes,
|
||||
self._config.unclean_exit_minutes)
|
||||
if unclean_exit_minutes is not None:
|
||||
if (minutes_since_launch > unclean_exit_minutes
|
||||
and not self._subprocess_sent_unclean_exit):
|
||||
opname = 'restart' if self._auto_restart else 'shutdown'
|
||||
print(
|
||||
f'{Clr.CYN}unclean_exit_minutes'
|
||||
f' ({unclean_exit_minutes})'
|
||||
f' elapsed; requesting immediate'
|
||||
f' {opname}.{Clr.RST}',
|
||||
flush=True)
|
||||
if self._auto_restart:
|
||||
self.restart(immediate=True)
|
||||
else:
|
||||
self.shutdown(immediate=True)
|
||||
self._subprocess_sent_unclean_exit = True
|
||||
|
||||
def _reset_subprocess_vars(self) -> None:
|
||||
self._subprocess = None
|
||||
self._subprocess_launch_time = None
|
||||
self._subprocess_sent_config_auto_restart = False
|
||||
self._subprocess_sent_clean_exit = False
|
||||
self._subprocess_sent_unclean_exit = False
|
||||
self._subprocess_force_kill_time = None
|
||||
self._subprocess_exited_cleanly = None
|
||||
|
||||
def _kill_subprocess(self) -> None:
|
||||
"""End the server subprocess if it still exists."""
|
||||
assert current_thread() is self._subprocess_thread
|
||||
if self._subprocess is None:
|
||||
return
|
||||
|
||||
print(f'{Clr.CYN}Stopping subprocess...{Clr.RST}', flush=True)
|
||||
|
||||
# First, ask it nicely to die and give it a moment.
|
||||
# If that doesn't work, bring down the hammer.
|
||||
self._subprocess.terminate()
|
||||
try:
|
||||
self._subprocess.wait(timeout=10)
|
||||
self._subprocess_exited_cleanly = (
|
||||
self._subprocess.returncode == 0)
|
||||
except subprocess.TimeoutExpired:
|
||||
self._subprocess_exited_cleanly = False
|
||||
self._subprocess.kill()
|
||||
print(f'{Clr.CYN}Subprocess stopped.{Clr.RST}', flush=True)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Run the BallisticaCore server manager."""
|
||||
try:
|
||||
ServerManagerApp().run()
|
||||
except CleanError as exc:
|
||||
# For clean errors, do a simple print and fail; no tracebacks/etc.
|
||||
# Any others will bubble up and give us the usual mess.
|
||||
exc.pretty_print()
|
||||
sys.exit(1)
|
||||
|
||||
def dump_logs(msg):
|
||||
if os.path.isfile('logs.log'):
|
||||
size = os.path.getsize('logs.log')
|
||||
|
||||
if size > 2000000:
|
||||
os.remove('logs.log')
|
||||
|
||||
with open("logs.log", "a") as f:
|
||||
f.write(msg)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -607,7 +607,7 @@ class ServerManagerApp:
|
|||
# run under us. This causes it to ignore ctrl-c presses and other
|
||||
# slight behavior tweaks. Hmm; should this be an argument instead?
|
||||
os.environ['BA_SERVER_WRAPPER_MANAGED'] = '1'
|
||||
|
||||
os.environ['BA_HOST_NAME'] = self._config.party_name
|
||||
print(f'{Clr.CYN}Launching server subprocess...{Clr.RST}', flush=True)
|
||||
binary_name = ('BallisticaCoreHeadless.exe'
|
||||
if os.name == 'nt' else './bombsquad_headless')
|
||||
|
|
|
|||
13
dist/ba_data/data/langdata.json
vendored
13
dist/ba_data/data/langdata.json
vendored
|
|
@ -78,7 +78,7 @@
|
|||
"adan",
|
||||
"Adeel (AdeZ {@adez_})",
|
||||
"Adel",
|
||||
"Rio adi",
|
||||
"Rio Adi",
|
||||
"Rayhan Adiansyah",
|
||||
"Yonas Adiel",
|
||||
"admin",
|
||||
|
|
@ -159,6 +159,7 @@
|
|||
"amr",
|
||||
"Anandchaursiya",
|
||||
"Anas",
|
||||
"Anastasija",
|
||||
"AnatoliyanChubukov",
|
||||
"AnatoliyanChybycov",
|
||||
"Bryan Enrique Perez de Anda",
|
||||
|
|
@ -184,6 +185,7 @@
|
|||
"Aniol",
|
||||
"Anmol",
|
||||
"anonymous",
|
||||
"Alok. R. Anoop",
|
||||
"Antonio",
|
||||
"Antoniom",
|
||||
"Lucas Antunes",
|
||||
|
|
@ -1107,7 +1109,7 @@
|
|||
"Nikita Oleshko",
|
||||
"Omar",
|
||||
"On3GaMs",
|
||||
"No one",
|
||||
"No One",
|
||||
"Adam Oros",
|
||||
"Andrés Ortega",
|
||||
"Zangar Orynbetov",
|
||||
|
|
@ -1292,6 +1294,7 @@
|
|||
"Bsam bu salh",
|
||||
"M. Rizki Agus Salim",
|
||||
"Salted",
|
||||
"Matteo Salvini",
|
||||
"Salvo04",
|
||||
"San",
|
||||
"SaNt0RiNiKits577YT",
|
||||
|
|
@ -1347,6 +1350,7 @@
|
|||
"Nikhil sohan",
|
||||
"SoK",
|
||||
"SoldierBS",
|
||||
"Unnamed Solicitude",
|
||||
"SPT Sosat",
|
||||
"Soto",
|
||||
"spacechase26",
|
||||
|
|
@ -1485,6 +1489,7 @@
|
|||
"vinoth",
|
||||
"Vishal",
|
||||
"Voxel",
|
||||
"Voxel25",
|
||||
"VTOR",
|
||||
"Fernando Véliz",
|
||||
"Vít",
|
||||
|
|
@ -1527,6 +1532,7 @@
|
|||
"xxonx8",
|
||||
"Ajeet yadav",
|
||||
"yahya",
|
||||
"Arda Yalın",
|
||||
"Yamir",
|
||||
"YannSonic",
|
||||
"Yantohrmnt401",
|
||||
|
|
@ -1542,6 +1548,7 @@
|
|||
"Yovan182Sunbreaker",
|
||||
"Yrtking",
|
||||
"All Star YT",
|
||||
"Dark Fgg5 YT",
|
||||
"Yudhis",
|
||||
"yugo",
|
||||
"yullian",
|
||||
|
|
@ -1594,6 +1601,7 @@
|
|||
"Даниил",
|
||||
"данил",
|
||||
"дибисяра",
|
||||
"Дмитрий 228",
|
||||
"Евгений(Eugene)",
|
||||
"Артём Зобков (KoLenka)",
|
||||
"Юстин Иглин",
|
||||
|
|
@ -1705,6 +1713,7 @@
|
|||
"药药Medic",
|
||||
"蔚蓝枫叶",
|
||||
"陈星宇你就是歌姬吧",
|
||||
"随风飘姚",
|
||||
"鲨鱼服·Medic",
|
||||
"鲲鹏元帅",
|
||||
"꧁ℤephyro꧂",
|
||||
|
|
|
|||
2
dist/ba_data/data/languages/chinese.json
vendored
2
dist/ba_data/data/languages/chinese.json
vendored
|
|
@ -15,6 +15,7 @@
|
|||
"linkAccountsInstructionsText": "若要关联两个账户,在其中一个账户内\n生成一个代码,用以在另一个账户内输入。\n游戏进程和物品将会被合并。\n您最多可以关联${COUNT}个账户\n\n重要:只能关联您自己的帐户!\n如果您跟您的朋友关联帐户\n您将无法在同一时间玩\n\n另外:此操作目前不能撤销,所以要小心!",
|
||||
"linkAccountsText": "关联账户",
|
||||
"linkedAccountsText": "已关联的账户:",
|
||||
"manageAccountText": "管理账户",
|
||||
"nameChangeConfirm": "更改账户名称为${NAME}?",
|
||||
"notLoggedInText": "未登录",
|
||||
"resetProgressConfirmNoAchievementsText": "这样会重置单机模式的进程和\n本地的高分记录(并不包括你的点券),\n而且不能恢复,确定要这样做吗?",
|
||||
|
|
@ -1009,6 +1010,7 @@
|
|||
"creditsText": "制作团队",
|
||||
"demoMenuText": "演示菜单",
|
||||
"endGameText": "结束",
|
||||
"endTestText": "结束测试",
|
||||
"exitGameText": "退出",
|
||||
"exitToMenuText": "退出到菜单",
|
||||
"howToPlayText": "帮助",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"linkAccountsInstructionsNewText": "要連結兩個帳號,在第一個帳號里點擊“生成代碼”按鈕。\n在第二個帳號裡輸入那個代碼。\n兩個帳號數據將被共享。\n(第一個帳號的數據將會消失)\n\n您總共可連結${COUNT}個帳號。\n\n重要訊息:你只能連結你自己的帳號;\n如果你連結了朋友的帳號, \n你將無法同時線上遊玩。",
|
||||
"linkAccountsText": "連結帳號",
|
||||
"linkedAccountsText": "已連結的帳號:",
|
||||
"manageAccountText": "管理賬戶",
|
||||
"nameChangeConfirm": "更改帳號名為${NAME}?",
|
||||
"resetProgressConfirmNoAchievementsText": "這會重設你的合作模式進度和\n本地高分紀錄 (但不包含你的票卷)。\n這是不可回復的操作,你確定嗎?",
|
||||
"resetProgressConfirmText": "這會重設你的合作模式進度,\n成就和本地高分\n(但不包括你的票卷)。這是不可\n回復的操作。你確定嗎?",
|
||||
|
|
@ -495,6 +496,7 @@
|
|||
"welcome2Text": "你還可以參加很多相同活動來贏取點券\n點券可以用於解鎖新角色、地圖、圖標\n和迷你遊戲,或購買錦標賽門票",
|
||||
"yourPowerRankingText": "你的能力排名:"
|
||||
},
|
||||
"copyConfirmText": "複製到剪切板",
|
||||
"copyOfText": "${NAME} 拷貝",
|
||||
"copyText": "複製",
|
||||
"createEditPlayerText": "<創建/編輯玩家>",
|
||||
|
|
@ -995,6 +997,7 @@
|
|||
"creditsText": "製作團隊",
|
||||
"demoMenuText": "演示菜單",
|
||||
"endGameText": "結束遊戲",
|
||||
"endTestText": "結束測試",
|
||||
"exitGameText": "退出遊戲",
|
||||
"exitToMenuText": "退出到菜單?",
|
||||
"howToPlayText": "幫助",
|
||||
|
|
|
|||
5
dist/ba_data/data/languages/czech.json
vendored
5
dist/ba_data/data/languages/czech.json
vendored
|
|
@ -15,6 +15,7 @@
|
|||
"linkAccountsInstructionsText": "Pokud chcete propojit účty, na jednom z nich generujte kód\na na druhém tento kód zadejte.\nPostup a inventář bude zkombinován.\nMůžete propojit až ${COUNT} účtů.\n\nDŮLEŽITÉ: PROPOJUJTE POUZE ÚČTY KTERÉ JSOU VAŠE!\nPOKUD PROPOJÍTE ÚČTY S KAMARÁDEM, NEBUDETE MOCI\nHRÁT OBA VE STEJNÝ ČAS!\n\nDobře si to rozmyslete; Tuto akci nelze vrátit zpět!",
|
||||
"linkAccountsText": "Propojit účty",
|
||||
"linkedAccountsText": "Propojené účty:",
|
||||
"manageAccountText": "Spravovat Účet",
|
||||
"nameChangeConfirm": "Přejete si změnit jméno účtu na ${NAME}?",
|
||||
"notLoggedInText": "<nejste přihlášen>",
|
||||
"resetProgressConfirmNoAchievementsText": "Jste si jistí, že to chcete udělat?\nTímto NENÁVRATNĚ resetujete veškerý postup Co-op\na lokální nejlepší výsledky (ale kupóny vám zůstanou).",
|
||||
|
|
@ -505,6 +506,7 @@
|
|||
"welcome2Text": "Také můžete získávat kupóny z mnoha aktivit.\nMohou být použity pro odemykání nových postav, map,\nmini-her, pro vstup do turnajů, a dalších.",
|
||||
"yourPowerRankingText": "Vaše pozice:"
|
||||
},
|
||||
"copyConfirmText": "Zkopírováno do schránky.",
|
||||
"copyOfText": "${NAME} Kopie",
|
||||
"copyText": "Kopírovat",
|
||||
"createEditPlayerText": "<Vyrobit/Upravit hráče>",
|
||||
|
|
@ -634,7 +636,7 @@
|
|||
"epicDescriptionFilterText": "${DESCRIPTION} V epickém slow motionu",
|
||||
"epicNameFilterText": "Epické ${NAME}",
|
||||
"errorAccessDeniedText": "přístup zamítnut",
|
||||
"errorDeviceTimeIncorrectText": "Čas vašeho zařízení je mimo o ${HOURS} h.\nTo pravděpodobně způsobí problémy.\nProsím zkontrolujte nastavení času a časového pásma.",
|
||||
"errorDeviceTimeIncorrectText": "Čas vašeho zařízení je špatně o ${HOURS} h.\nTo pravděpodobně způsobí problémy.\nProsím zkontrolujte nastavení času a časového pásma.",
|
||||
"errorOutOfDiskSpaceText": "není místo na disku",
|
||||
"errorSecureConnectionFailText": "Unable to establish secure cloud connection; network functionality may fail.",
|
||||
"errorText": "Chyba",
|
||||
|
|
@ -1011,6 +1013,7 @@
|
|||
"creditsText": "Tvůrci",
|
||||
"demoMenuText": "Demo menu",
|
||||
"endGameText": "Konec Hry",
|
||||
"endTestText": "Konec Testu",
|
||||
"exitGameText": "Ukončit Hru",
|
||||
"exitToMenuText": "Vrátit se do menu?",
|
||||
"howToPlayText": "Jak hrát",
|
||||
|
|
|
|||
2
dist/ba_data/data/languages/danish.json
vendored
2
dist/ba_data/data/languages/danish.json
vendored
|
|
@ -1174,7 +1174,7 @@
|
|||
"winterSpecialText": "Vinterspecials",
|
||||
"youOwnThisText": "- Du ejer dette -"
|
||||
},
|
||||
"storeDescriptionText": "Vanvittigt spil med op plads til op til 8 spillere!\n\nSpræng dine venner eller computeren i luften i en turnering af eksplosive mini-spil som f.eks. find fanen, bombe-hockey og kamp til døden i slowmotion.\n\nNem styring og omfattende controllersupport gør det nemt at få op til 8 spillere med i kampen! Du kan endda bruge din mobil som controller via den gratis 'BombSquad Remote'-app.\n\nDer skydes!\n\nTjek www.froemling.net/bomsquad ud for mere information.",
|
||||
"storeDescriptionText": "Vanvittigt spil med op plads til op til 8 spillere!\n\nSpræng dine venner eller computeren i luften i en turnering af eksplosive mini-spil som f.eks. find fanen, bombe-hockey og kamp til døden i slowmotion.\n\nNem styring og omfattende controllersupport gør det nemt at få op til 8 spillere med i kampen! Du kan endda bruge din mobil som controller via den gratis 'BombSquad Remote'-app.\n\n\n\nTjek bombsquadgame.com ud for mere information.",
|
||||
"storeDescriptions": {
|
||||
"blowUpYourFriendsText": "Spræng dine venner i luften",
|
||||
"competeInMiniGamesText": "Konkurrer i mini-spil lige fra racing til at flyve.",
|
||||
|
|
|
|||
2
dist/ba_data/data/languages/english.json
vendored
2
dist/ba_data/data/languages/english.json
vendored
|
|
@ -13,6 +13,7 @@
|
|||
"linkAccountsInstructionsNewText": "To link two accounts, generate a code on the first\nand enter that code on the second. Data from the\nsecond account will then be shared between both.\n(Data from the first account will be lost)\n\nYou can link up to ${COUNT} accounts.\n\nIMPORTANT: only link accounts that you own;\nIf you link with friends’ accounts you will not\nbe able to play online at the same time.",
|
||||
"linkAccountsText": "Link Accounts",
|
||||
"linkedAccountsText": "Linked Accounts:",
|
||||
"manageAccountText": "Manage Account",
|
||||
"nameChangeConfirm": "Change your account name to ${NAME}?",
|
||||
"resetProgressConfirmNoAchievementsText": "This will reset your co-op progress and\nlocal high-scores (but not your tickets).\nThis cannot be undone. Are you sure?",
|
||||
"resetProgressConfirmText": "This will reset your co-op progress,\nachievements, and local high-scores\n(but not your tickets). This cannot\nbe undone. Are you sure?",
|
||||
|
|
@ -1006,6 +1007,7 @@
|
|||
"creditsText": "Credits",
|
||||
"demoMenuText": "Demo Menu",
|
||||
"endGameText": "End Game",
|
||||
"endTestText": "End Test",
|
||||
"exitGameText": "Exit Game",
|
||||
"exitToMenuText": "Exit to menu?",
|
||||
"howToPlayText": "How to Play",
|
||||
|
|
|
|||
2
dist/ba_data/data/languages/filipino.json
vendored
2
dist/ba_data/data/languages/filipino.json
vendored
|
|
@ -14,6 +14,7 @@
|
|||
"linkAccountsInstructionsText": "Para mag-ugnay ng dalawang account, gumawa ng code\nsa isa at ilagay ang code na iyon sa kabila.\nAng iyong pag-usad at imbentaryo ay pagsasamahin.\nMaaari mong i-ugnay hanggang sa ${COUNT} accounts.\n\nBabala lang; hindi na ito maibabalik!",
|
||||
"linkAccountsText": "Iugnay ang mga Account",
|
||||
"linkedAccountsText": "Naka-ugnay na mga Account",
|
||||
"manageAccountText": "Patnubayan ang Account",
|
||||
"nameChangeConfirm": "Baguhin ang pangalan ng iyong account sa ${NAME}?",
|
||||
"resetProgressConfirmNoAchievementsText": "Ibabalik nito sa dati ang iyong pag-usad,\nat lokal na mga high-score (pwera sa ticket).\nHindi na ito maibabalik pa. Ipagpatuloy pa rin?",
|
||||
"resetProgressConfirmText": "Ibabalik nito sa dati ang iyong pag-usad,\nmga nakamtan, at lokal na mga high-score\n(pwera sa ticket). Hindi na ito maibabalik\npa. Ipagpatuloy pa rin?",
|
||||
|
|
@ -1001,6 +1002,7 @@
|
|||
"creditsText": "Mga Kredito",
|
||||
"demoMenuText": "Demo na Menu",
|
||||
"endGameText": "Itigil ang Laro",
|
||||
"endTestText": "Itigil ang Test",
|
||||
"exitGameText": "Umalis sa Laro",
|
||||
"exitToMenuText": "Balik sa menu?",
|
||||
"howToPlayText": "Paano Maglaro",
|
||||
|
|
|
|||
4
dist/ba_data/data/languages/french.json
vendored
4
dist/ba_data/data/languages/french.json
vendored
|
|
@ -15,6 +15,7 @@
|
|||
"linkAccountsInstructionsText": "Pour lier deux comptes, générez un code sur l'un\nd'entre eux et entrer ce code sur l'autre.\nLes progrès et l'inventaire seront combinés.\nVous pouvez lier jusqu'à ${COUNT} comptes.\n\nIMPORTANT: reliez uniquement les comptes que vous possédez!\nSi vous reliez des comptes à vos amis, vous\nne pourrais pas jouer en même temps!\n\nAussi: cela ne peut actuellement pas être annulé, alors faites attention!",
|
||||
"linkAccountsText": "Lier vos comptes",
|
||||
"linkedAccountsText": "Comptes Liés:",
|
||||
"manageAccountText": "Gérer compte",
|
||||
"nameChangeConfirm": "Changer le nom de votre compte pour ${NAME}?",
|
||||
"notLoggedInText": "<pas connecté>",
|
||||
"resetProgressConfirmNoAchievementsText": "Ceci réinitialisera votre progression co-op et\nvos meilleurs scores locaux (mais pas vos tickets).\nCette action est irréversible. Êtes-vous certain(e)?",
|
||||
|
|
@ -1056,6 +1057,7 @@
|
|||
"creditsText": "Crédits",
|
||||
"demoMenuText": "Menu Démo",
|
||||
"endGameText": "Terminer le jeu",
|
||||
"endTestText": "Terminer test",
|
||||
"exitGameText": "Quitter le jeu",
|
||||
"exitToMenuText": "Retourner au menu?",
|
||||
"howToPlayText": "Guide Pratique",
|
||||
|
|
@ -1328,7 +1330,7 @@
|
|||
"translationEditorButtonText": "Éditeur des Traductions de ${APP_NAME}",
|
||||
"translationFetchErrorText": "statut de la traduction indisponible",
|
||||
"translationFetchingStatusText": "vérification du statut de la traduction...",
|
||||
"translationInformMe": "Informez-moi quand ma langue a besoin de mises à jour",
|
||||
"translationInformMe": "M'avertir quand ma langue a besoin de mises à jour",
|
||||
"translationNoUpdateNeededText": "ce langage est à jour; woohoo!",
|
||||
"translationUpdateNeededText": "** ce langage à besoin des modifications!! **",
|
||||
"vrTestingText": "Test de la RV"
|
||||
|
|
|
|||
2
dist/ba_data/data/languages/gibberish.json
vendored
2
dist/ba_data/data/languages/gibberish.json
vendored
|
|
@ -17,6 +17,7 @@
|
|||
"linkAccountsInstructionsText": "Tz lknf twz aoc coj, goi woc woef \nacoweof oac oia oow towjoifowj\nProwfo and in oir wojoc owinofiff.\nYouc owo iwoe oijf ${COUNT} accoaoijf.\n\nIMPCO oOIFOWEFJ\nowijecojwef\noijoicjwe\n\nAOicjowijeojwoeifjwef",
|
||||
"linkAccountsText": "Lnk Accnffzz",
|
||||
"linkedAccountsText": "Lnkfdf Accnrts:",
|
||||
"manageAccountText": "JCowejerwoerjfwff",
|
||||
"nameChangeConfirm": "Cho weft cowejf coiwjocwe. ${NAME}?",
|
||||
"notLoggedInText": "<nzt lggzt inz>",
|
||||
"resetProgressConfirmNoAchievementsText": "Thzz wflzz resta yrsr co-opz proglfzlz\nand hghzl scrdz. Itz cntljdf be zunfzz.\nArz yoz srz?",
|
||||
|
|
@ -1081,6 +1082,7 @@
|
|||
"creditsText": "Crizetzzts",
|
||||
"demoMenuText": "Dmzm Mnzz",
|
||||
"endGameText": "Enzs Gzmé´fez",
|
||||
"endTestText": "Erj coiwjef",
|
||||
"exitGameText": "Excczzt Gamzzéé",
|
||||
"exitToMenuText": "Exít tzú ménu?",
|
||||
"howToPlayText": "Hwopm T´wPlayynf",
|
||||
|
|
|
|||
2
dist/ba_data/data/languages/greek.json
vendored
2
dist/ba_data/data/languages/greek.json
vendored
|
|
@ -14,6 +14,7 @@
|
|||
"linkAccountsInstructionsText": "Για να ενώσετε δυο λογαριασμούς, δημιουργήστε κωδικό\nσε έναν από αυτούς και εισάγετέ τον στον άλλο.\nΗ πρόοδος και τα περιεχόμενα θα συγχωνεύθουν.\nΜπορείτε να ενώσεις μέχρι και ${COUNT} λογαριασμούς.\n\nΠροσοχή: η ενέργεια είναι μη αναστρέψιμη!\n\n",
|
||||
"linkAccountsText": "Δεσμεύστε Λογαριασμούς",
|
||||
"linkedAccountsText": "Δεσεμευμένοι λογαριασμοί:",
|
||||
"manageAccountText": "Διαχείρειση λογαριασμού",
|
||||
"nameChangeConfirm": "Αλλαγή του ονόματος του λογαριασμού σας σε ${NAME};",
|
||||
"resetProgressConfirmNoAchievementsText": "Αυτό θα επαναφέρει τη πρόοδο σας στη \"συνεργασία\" και τις\nτοπικές υψηλές σας βαθμολογίες (αλλά όχι τα εισιτήρια).\nΗ ενέργεια είναι μη αντιστρέψιμη. Είστε σίγουροι;",
|
||||
"resetProgressConfirmText": "Αυτό θα επαναφέρει τη πρόοδο σας στη \"συνεργασία\",\nτα επιτεύγματα και τις τοπικές υψηλές σας\nβαθμολογίες (αλλά όχι τα εισιτήρια). Η ενέργεια\nείναι μη αντιστρέψιμη. Είστε σίγουροι;",
|
||||
|
|
@ -1002,6 +1003,7 @@
|
|||
"creditsText": "Εύσημα",
|
||||
"demoMenuText": "Μενού Επίδειξης",
|
||||
"endGameText": "Τέλος Παιχνιδιού",
|
||||
"endTestText": "Τέλος τεστ",
|
||||
"exitGameText": "Έξοδος Παιχνιδιού",
|
||||
"exitToMenuText": "Έξοδος στο μενού;",
|
||||
"howToPlayText": "Πως Παίζεται;",
|
||||
|
|
|
|||
2
dist/ba_data/data/languages/indonesian.json
vendored
2
dist/ba_data/data/languages/indonesian.json
vendored
|
|
@ -15,6 +15,7 @@
|
|||
"linkAccountsInstructionsText": "Untuk menghubungkan dua akun, buat kode di salah satu\ndari mereka dan masukkan kode itu di sisi lain.\nKemajuan dan persediaan akan digabungkan.\nAnda dapat menghubungkan hingga ${COUNT} akun.\n\nPENTING: Hanya hubungkan akun yang anda miliki!\nJika Anda menghubungkan akun dengan teman anda, anda\ntidak akan bisa bermain bersamaan!\n\nJuga: ini tidak dapat dibatalkan, jadi berhati-hatilah!",
|
||||
"linkAccountsText": "Hubungkan Akun",
|
||||
"linkedAccountsText": "Akun yang Terhubung:",
|
||||
"manageAccountText": "Pengaturan Akun",
|
||||
"nameChangeConfirm": "Ganti namamu menjadi ${NAME}?",
|
||||
"resetProgressConfirmNoAchievementsText": "Tindakan ini akan mengatur ulang kemajuan co-op dan\nskor tertinggi Kamu (kecuali tiket).\nTidak dapat dibatalkan. Apakah Kamu yakin?",
|
||||
"resetProgressConfirmText": "Ini akan mengatur ulang kemajuan co-op,\npencapaian, dan skor tertinggi Kamu\n(kecuali tiket). Ini tidak dapat\ndibatalkan. Kamu yakin?",
|
||||
|
|
@ -1000,6 +1001,7 @@
|
|||
"creditsText": "Kredit",
|
||||
"demoMenuText": "Menu Demo",
|
||||
"endGameText": "Akhiri Permainan",
|
||||
"endTestText": "Akhiri Pengetesan",
|
||||
"exitGameText": "Keluar dari Permainan",
|
||||
"exitToMenuText": "Ke Menu?",
|
||||
"howToPlayText": "Cara bermain",
|
||||
|
|
|
|||
2
dist/ba_data/data/languages/italian.json
vendored
2
dist/ba_data/data/languages/italian.json
vendored
|
|
@ -16,6 +16,7 @@
|
|||
"linkAccountsInstructionsText": "Per collegare due account, crea un codice su uno\ndei dispositivi e inserisci quel codice negli altri. \nProgressi e inventario verranno combinati.\nPuoi collegare fino a ${COUNT} account.\n\nIMPORTANTE: Collega solo account che possiedi!\nSe colleghi un account con un tuo amico non potrete\ngiocare allo stesso momento!\n \nInoltre: questa operazione non può essere annullata, quindi stai attento!",
|
||||
"linkAccountsText": "Collega account",
|
||||
"linkedAccountsText": "Account Collegati:",
|
||||
"manageAccountText": "Gestisci account",
|
||||
"nameChangeConfirm": "Confermi di voler modificare il tuo nome in ${NAME}?",
|
||||
"notLoggedInText": "<accesso non effettuato>",
|
||||
"resetProgressConfirmNoAchievementsText": "Stai per cancellare i tuoi progressi in\nmodalità cooperativa, i tuoi obbiettivi, i tuoi punteggi\nlocali (ma non i tuoi biglietti). \n\nL'operazione è irreversibile: continuare?",
|
||||
|
|
@ -1045,6 +1046,7 @@
|
|||
"creditsText": "Riconoscimenti",
|
||||
"demoMenuText": "Menu Demo",
|
||||
"endGameText": "Chiudi partita",
|
||||
"endTestText": "Termina Test",
|
||||
"exitGameText": "Esci dal gioco",
|
||||
"exitToMenuText": "Tornare al menu?",
|
||||
"howToPlayText": "Come Giocare",
|
||||
|
|
|
|||
2
dist/ba_data/data/languages/portuguese.json
vendored
2
dist/ba_data/data/languages/portuguese.json
vendored
|
|
@ -15,6 +15,7 @@
|
|||
"linkAccountsInstructionsText": "Para vincular duas contas, gere um código\nem uma delas e o insira na outra.\nProgresso e inventário serão combinados.\nVocê pode vincular até ${COUNT} contas.\n\nIMPORTANTE: Vincule apenas as contas que você possui!\nSe você vincular contas a seus amigos, você\nnão será capaz de jogar ao mesmo tempo!\n\nAlém disso: isso não pode ser desfeito atualmente, então tenha cuidado!",
|
||||
"linkAccountsText": "Vincular contas",
|
||||
"linkedAccountsText": "Contas vinculadas:",
|
||||
"manageAccountText": "Gerenciar Conta",
|
||||
"nameChangeConfirm": "Mudar o nome da sua conta para ${NAME}?",
|
||||
"notLoggedInText": "<não logado>",
|
||||
"resetProgressConfirmNoAchievementsText": "Isto reiniciará o seu progresso no cooperativo e\nas suas pontuações (mas não os seus bilhetes).\nIsto não pode ser desfeito. Tem certeza?",
|
||||
|
|
@ -1066,6 +1067,7 @@
|
|||
"creditsText": "Créditos",
|
||||
"demoMenuText": "Menu de demonstração",
|
||||
"endGameText": "Finalizar jogo",
|
||||
"endTestText": "Finalizar Teste",
|
||||
"exitGameText": "Sair do jogo",
|
||||
"exitToMenuText": "Sair para o menu?",
|
||||
"howToPlayText": "Como jogar",
|
||||
|
|
|
|||
2
dist/ba_data/data/languages/russian.json
vendored
2
dist/ba_data/data/languages/russian.json
vendored
|
|
@ -16,6 +16,7 @@
|
|||
"linkAccountsInstructionsText": "Для связки двух аккаунтов, создайте код на одном\nиз них и введите код на другом.\nПрогресс и инвентарь будут объединены.\nВы можете связать до ${COUNT} аккаунтов.",
|
||||
"linkAccountsText": "Связать акаунты",
|
||||
"linkedAccountsText": "Привязанные аккаунты:",
|
||||
"manageAccountText": "Управление аккаунтом",
|
||||
"nameChangeConfirm": "Вы уверены, что хотите сменить имя аккаунта на ${NAME}?",
|
||||
"notLoggedInText": "<не авторизован>",
|
||||
"resetProgressConfirmNoAchievementsText": "Это сбросит весь ваш кооперативный прогресс\nи локальные рекорды (но не билеты).\nЭтот процесс необратим. Вы уверены?",
|
||||
|
|
@ -1049,6 +1050,7 @@
|
|||
"creditsText": "Благодарности",
|
||||
"demoMenuText": "Меню демо",
|
||||
"endGameText": "Закончить игру",
|
||||
"endTestText": "Завершить тест",
|
||||
"exitGameText": "Выйти из игры",
|
||||
"exitToMenuText": "Выйти в меню?",
|
||||
"howToPlayText": "Как играть",
|
||||
|
|
|
|||
2
dist/ba_data/data/languages/spanish.json
vendored
2
dist/ba_data/data/languages/spanish.json
vendored
|
|
@ -16,6 +16,7 @@
|
|||
"linkAccountsInstructionsText": "Para enlazar dos cuentas, genera un código en una\n de ellas y escribe el código en la otra.\nEl progreso e accesorios se combinarán.\nPuedes enlazar hasta ${COUNT} cuentas. \n\nIMPORTANTE: Solo enlaza cuentas tuyas!\n\nSi enlazas cuentas con tus amigos no podrán jugar al mismo tiempo!\n\nTambién: esto no se puede deshacer actualmente, así que se cuidadoso!",
|
||||
"linkAccountsText": "Enlazar Cuentas",
|
||||
"linkedAccountsText": "Cuentas enlazadas:",
|
||||
"manageAccountText": "administrar cuenta",
|
||||
"nameChangeConfirm": "¿Cambiar tu nombre a ${NAME}?",
|
||||
"notLoggedInText": "<no estás conectado>",
|
||||
"resetProgressConfirmNoAchievementsText": "Esto reiniciará tu progreso en el modo\ncooperativo y tus puntajes (A excepción de tus tickets).\nNo podrás recuperar los cambios. ¿Estás seguro?",
|
||||
|
|
@ -1062,6 +1063,7 @@
|
|||
"creditsText": "Créditos",
|
||||
"demoMenuText": "Menú Demo",
|
||||
"endGameText": "Salir del juego",
|
||||
"endTestText": "Prueba final",
|
||||
"exitGameText": "Salir del juego",
|
||||
"exitToMenuText": "¿Salir al menú?",
|
||||
"howToPlayText": "Cómo jugar",
|
||||
|
|
|
|||
2
dist/ba_data/data/languages/tamil.json
vendored
2
dist/ba_data/data/languages/tamil.json
vendored
|
|
@ -13,6 +13,7 @@
|
|||
"linkAccountsInstructionsNewText": "இரண்டு கணக்குகளை இணைக்க, முதலில் ஒரு குறியீட்டை உருவாக்கவும்\nஇரண்டாவது குறியீட்டை உள்ளிடவும். \n இலிருந்து தரவு\nஇரண்டாவது கணக்கு பின்னர் இருவருக்கும் இடையே பகிரப்படும். \n(முதல் கணக்கிலிருந்து தரவு இழக்கப்படும்)\n\nநீங்கள் ${COUNT} கணக்குகள் வரை இணைக்க முடியும்.\n\nமுக்கியம்: உங்களுக்கு சொந்தமான கணக்குகளை மட்டுமே இணைக்கவும்;\nநண்பர்களின் கணக்குகளுடன் நீங்கள் இணைத்தால் நீங்கள் ஒரே நேரத்தில் ஆன்லைனில் விளையாட முடியாது.",
|
||||
"linkAccountsText": "கணக்குகளை இணைக்கவும்",
|
||||
"linkedAccountsText": "இணைக்கப்பட்ட கணக்குகள்:",
|
||||
"manageAccountText": "கணக்கை நிர்வகி",
|
||||
"nameChangeConfirm": "உங்கள் கணக்கின் பெயரை ${NAME}க்கு மாற்ற",
|
||||
"resetProgressConfirmNoAchievementsText": "இது உங்கள் கூட்டு முன்னேற்றங்கள் மற்றும் கருவி உயர்மதிப்பெண்கள் ஆகியவற்றை அழிக்கும் (ஆனால் உங்கள் சீட்டுகள் அல்ல).\nஇச்செயலை பின்வாங்க முடியாது.\nஉறுதியா?",
|
||||
"resetProgressConfirmText": "இது உங்கள் கூட்டு முன்னேற்றங்கள், சாதனைகள், மற்றும் கருவி உயர்மதிப்பெண்கள் ஆகியவற்றை அழிக்கும் \n(ஆனால் உங்கள் சீட்டுகள் அல்ல).\nஇச்செயலை பின்வாங்க முடியாது.\nஉறுதியா?",
|
||||
|
|
@ -999,6 +1000,7 @@
|
|||
"creditsText": "வரவுகள்",
|
||||
"demoMenuText": "டெமோ மெனு",
|
||||
"endGameText": "விளையாட்டை வெளியேறு",
|
||||
"endTestText": "முடிவு சோதனை",
|
||||
"exitGameText": "விளையாட்டை வெளியேறு",
|
||||
"exitToMenuText": "மெனுவிலிருந்து வெளியேறவா?",
|
||||
"howToPlayText": "எப்படி விளையாடுவது",
|
||||
|
|
|
|||
2
dist/ba_data/data/languages/turkish.json
vendored
2
dist/ba_data/data/languages/turkish.json
vendored
|
|
@ -15,6 +15,7 @@
|
|||
"linkAccountsInstructionsText": "İki hesabı birbirine bağlamak için, birinde kod oluşturun\nve o kodu diğerinde girin.\nİlerlemeler ve envanterler birleşecektir.\n${COUNT} taneye kadar hesap bağlayabilirsiniz.\n\nÖNEMLİ: Yalnızca kendi hesaplarını bağla!\nEğer arkadaşınla hesapları bağlarsan\naynı anda oynayamayacaksınız!\n\nAyrıca: bu işlem şu anda geri alınamaz, yani dikkatli ol!",
|
||||
"linkAccountsText": "Hesapları Bağla",
|
||||
"linkedAccountsText": "Bağlı Hesaplar:",
|
||||
"manageAccountText": "Hesabı Yönet",
|
||||
"nameChangeConfirm": "Hesap adın ${NAME} olarak değiştirilsinmi ?",
|
||||
"resetProgressConfirmNoAchievementsText": "Bu co-op ilerlemeyi ve \nlokaldeki yüksek puanlarnı sıfırlar(ama biletlerini değil).\nBu geri alınamaz. Emin misiniz?",
|
||||
"resetProgressConfirmText": "Bu co-op ilerlemeyi, başarıları ve \nEn yüksek skorları sıfırlayacak \n(ama biletlerini değil). Bu işlem\ngeri alınamaz. Emin misin?",
|
||||
|
|
@ -999,6 +1000,7 @@
|
|||
"creditsText": "Jenerik",
|
||||
"demoMenuText": "Demo Menü",
|
||||
"endGameText": "Oyunu Bitir",
|
||||
"endTestText": "Testi Bitir",
|
||||
"exitGameText": "Oyundan Çık",
|
||||
"exitToMenuText": "Menüye Git?",
|
||||
"howToPlayText": "Nasıl Oynanır",
|
||||
|
|
|
|||
3
dist/ba_data/data/languages/ukrainian.json
vendored
3
dist/ba_data/data/languages/ukrainian.json
vendored
|
|
@ -15,6 +15,7 @@
|
|||
"linkAccountsInstructionsText": "Щоб зв'язати дви обликови записи, створювати код на одному з них и ввести цей код на инший. Прогрес та инвентаризации будуць об'эднанни. Ви можете зв'язати ${COUNT} рахунки.",
|
||||
"linkAccountsText": "Зв'язати акаунти",
|
||||
"linkedAccountsText": "Зв'язані акаунти:",
|
||||
"manageAccountText": "Контролювати аккаунтом",
|
||||
"nameChangeConfirm": "Ви впевнені що хочете змінити ім'я на ${NAME}?",
|
||||
"resetProgressConfirmNoAchievementsText": "Це скине весь ваш кооперативний прогрес\nі локальні кращі результати (крім квитків).\nЦей процес є незворотнім. Ви впевнені?",
|
||||
"resetProgressConfirmText": "Це скине весь ваш кооперативний\nпрогрес, досягнення і локальні результати\n(Крім квитків). Цей процес є незворотнім.\nВи впевнені?",
|
||||
|
|
@ -499,6 +500,7 @@
|
|||
"welcome2Text": "Ви також можете заробити квитки від багатьох однакових видів діяльності.\nКвитки можуть бути використані, щоб розблокувати нових персонажів, карти та\nміні-ігри, щоб увійти турніри, і багато іншого.",
|
||||
"yourPowerRankingText": "Ваш ранг:"
|
||||
},
|
||||
"copyConfirmText": "Скопійовано до буферу обміну",
|
||||
"copyOfText": "Копія ${NAME}",
|
||||
"copyText": "Копія",
|
||||
"createEditPlayerText": "<Створення / редагування гравця>",
|
||||
|
|
@ -999,6 +1001,7 @@
|
|||
"creditsText": "Подяки",
|
||||
"demoMenuText": "Меню прикладів",
|
||||
"endGameText": "Завершити гру",
|
||||
"endTestText": "Закінчити тест",
|
||||
"exitGameText": "Вийти з гри",
|
||||
"exitToMenuText": "Вийти в меню?",
|
||||
"howToPlayText": "Як грати",
|
||||
|
|
|
|||
6
dist/ba_data/data/languages/venetian.json
vendored
6
dist/ba_data/data/languages/venetian.json
vendored
|
|
@ -13,6 +13,7 @@
|
|||
"linkAccountsInstructionsNewText": "Par cołegar do account, jènara un còdaze inte'l primo\ndispozidivo e insarìseło inte’l segondo. I dati de’l\nsegondo account i vegnarà sparpagnài so tuti do i account\n(i dati de’l primo account i ndarà perdesti).\n\nTe połi cołegar fin a ${COUNT} account.\n\nINPORTANTE: cołega soło i account de to propiedà.\nSe te cołeghi account de calche to amigo, no sarè\npì boni de zugar online inte’l mèdemo momento.",
|
||||
"linkAccountsText": "Cołega account",
|
||||
"linkedAccountsText": "Account cołegài:",
|
||||
"manageAccountText": "Jestisi account",
|
||||
"nameChangeConfirm": "Vutu canbiar el nome de’l to account co ${NAME}?",
|
||||
"resetProgressConfirmNoAchievementsText": "Te si drio ełimenar i to progresi so ła modałidà\ncooparadiva e i to punteji łogałi (ma miga i to biłieti).\n’Sta asion no ła połe pì èsar anułada. Vutu ndar vanti?",
|
||||
"resetProgressConfirmText": "Te si drio ełimenar i to progresi so ła\nmodałidà cooparadiva, i to obietivi e i to punteji\nłogałi (ma miga i to biłieti). ’Sta asion\nno ła połe pì èsar anułada. Vutu ndar vanti?",
|
||||
|
|
@ -487,7 +488,7 @@
|
|||
"proMultInfoText": "I zugadori co'l mejoramento ${PRO}\ni beca el ${PERCENT}% de punti in pì.",
|
||||
"seeMoreText": "Clasìfega conpleta",
|
||||
"skipWaitText": "Vanti suito",
|
||||
"timeRemainingText": "Tenpo remanente",
|
||||
"timeRemainingText": "Tenpo che resta",
|
||||
"toRankedText": "par ndar sù de pozision",
|
||||
"totalText": "totałe",
|
||||
"tournamentInfoText": "Conpeti co cheł'altri zugadori par\nndar sù de puntejo inte ła to łega.\n\nCo'l tornèo el fenirà, i zugadori pì\nbrai i vegnarà reconpensài co i premi.",
|
||||
|
|
@ -996,6 +997,7 @@
|
|||
"creditsText": "Mension",
|
||||
"demoMenuText": "Menù demo",
|
||||
"endGameText": "Sara sù partìa",
|
||||
"endTestText": "Fenisi prova",
|
||||
"exitGameText": "Sortisi da'l zugo",
|
||||
"exitToMenuText": "Vutu tornar inte'l menù?",
|
||||
"howToPlayText": "Come zugar",
|
||||
|
|
@ -1238,7 +1240,7 @@
|
|||
"enablePackageModsText": "Ativa pacheti mod łogałi",
|
||||
"enterPromoCodeText": "Insarisi còdaze",
|
||||
"forTestingText": "Nota: vałori vàłidi soło par proe. Sortendo da l'apl łi vegnarà perdesti.",
|
||||
"helpTranslateText": "Łe tradusion de ${APP_NAME} łe ze curàe da vołontari.\nSe A te vol darghe na ociada a cheła veneta, struca so'l boton\ncuà soto. Curada da Còdaze Veneto: codazeveneto@gmail.com",
|
||||
"helpTranslateText": "Łe tradusion de ${APP_NAME} łe ze curàe da vołontari.\nSe te vołi darghe na ociada a cheła veneta, struca so'l boton\ncuà soto. Curada da VeC: venetianlanguage@gmail.com",
|
||||
"kickIdlePlayersText": "Para fora zugadori in sonera",
|
||||
"kidFriendlyModeText": "Modałidà bocia (viołensa reduzesta, evc)",
|
||||
"languageText": "Łengua",
|
||||
|
|
|
|||
67
dist/ba_data/python/_bainternal.py
vendored
67
dist/ba_data/python/_bainternal.py
vendored
|
|
@ -35,6 +35,7 @@ from typing import TYPE_CHECKING, TypeVar
|
|||
if TYPE_CHECKING:
|
||||
from typing import Any, Callable
|
||||
|
||||
|
||||
_T = TypeVar('_T')
|
||||
|
||||
|
||||
|
|
@ -44,13 +45,16 @@ def _uninferrable() -> Any:
|
|||
return _not_a_real_variable # type: ignore
|
||||
|
||||
|
||||
def add_transaction(transaction: dict,
|
||||
callback: Callable | None = None) -> None:
|
||||
def add_transaction(
|
||||
transaction: dict, callback: Callable | None = None
|
||||
) -> None:
|
||||
|
||||
"""(internal)"""
|
||||
return None
|
||||
|
||||
|
||||
def game_service_has_leaderboard(game: str, config: str) -> bool:
|
||||
|
||||
"""(internal)
|
||||
|
||||
Given a game and config string, returns whether there is a leaderboard
|
||||
|
|
@ -60,6 +64,7 @@ def game_service_has_leaderboard(game: str, config: str) -> bool:
|
|||
|
||||
|
||||
def get_master_server_address(source: int = -1, version: int = 1) -> str:
|
||||
|
||||
"""(internal)
|
||||
|
||||
Return the address of the master server.
|
||||
|
|
@ -68,66 +73,79 @@ def get_master_server_address(source: int = -1, version: int = 1) -> str:
|
|||
|
||||
|
||||
def get_news_show() -> str:
|
||||
|
||||
"""(internal)"""
|
||||
return str()
|
||||
|
||||
|
||||
def get_price(item: str) -> str | None:
|
||||
|
||||
"""(internal)"""
|
||||
return ''
|
||||
|
||||
|
||||
def get_public_login_id() -> str | None:
|
||||
|
||||
"""(internal)"""
|
||||
return ''
|
||||
|
||||
|
||||
def get_purchased(item: str) -> bool:
|
||||
|
||||
"""(internal)"""
|
||||
return bool()
|
||||
|
||||
|
||||
def get_purchases_state() -> int:
|
||||
|
||||
"""(internal)"""
|
||||
return int()
|
||||
|
||||
|
||||
def get_v1_account_display_string(full: bool = True) -> str:
|
||||
|
||||
"""(internal)"""
|
||||
return str()
|
||||
|
||||
|
||||
def get_v1_account_misc_read_val(name: str, default_value: Any) -> Any:
|
||||
|
||||
"""(internal)"""
|
||||
return _uninferrable()
|
||||
|
||||
|
||||
def get_v1_account_misc_read_val_2(name: str, default_value: Any) -> Any:
|
||||
|
||||
"""(internal)"""
|
||||
return _uninferrable()
|
||||
|
||||
|
||||
def get_v1_account_misc_val(name: str, default_value: Any) -> Any:
|
||||
|
||||
"""(internal)"""
|
||||
return _uninferrable()
|
||||
|
||||
|
||||
def get_v1_account_name() -> str:
|
||||
|
||||
"""(internal)"""
|
||||
return str()
|
||||
|
||||
|
||||
def get_v1_account_state() -> str:
|
||||
|
||||
"""(internal)"""
|
||||
return str()
|
||||
|
||||
|
||||
def get_v1_account_state_num() -> int:
|
||||
|
||||
"""(internal)"""
|
||||
return int()
|
||||
|
||||
|
||||
def get_v1_account_ticket_count() -> int:
|
||||
|
||||
"""(internal)
|
||||
|
||||
Returns the number of tickets for the current account.
|
||||
|
|
@ -136,31 +154,37 @@ def get_v1_account_ticket_count() -> int:
|
|||
|
||||
|
||||
def get_v1_account_type() -> str:
|
||||
|
||||
"""(internal)"""
|
||||
return str()
|
||||
|
||||
|
||||
def get_v2_fleet() -> str:
|
||||
|
||||
"""(internal)"""
|
||||
return str()
|
||||
|
||||
|
||||
def have_outstanding_transactions() -> bool:
|
||||
|
||||
"""(internal)"""
|
||||
return bool()
|
||||
|
||||
|
||||
def in_game_purchase(item: str, price: int) -> None:
|
||||
|
||||
"""(internal)"""
|
||||
return None
|
||||
|
||||
|
||||
def is_blessed() -> bool:
|
||||
|
||||
"""(internal)"""
|
||||
return bool()
|
||||
|
||||
|
||||
def mark_config_dirty() -> None:
|
||||
|
||||
"""(internal)
|
||||
|
||||
Category: General Utility Functions
|
||||
|
|
@ -169,36 +193,43 @@ def mark_config_dirty() -> None:
|
|||
|
||||
|
||||
def power_ranking_query(callback: Callable, season: Any = None) -> None:
|
||||
|
||||
"""(internal)"""
|
||||
return None
|
||||
|
||||
|
||||
def purchase(item: str) -> None:
|
||||
|
||||
"""(internal)"""
|
||||
return None
|
||||
|
||||
|
||||
def report_achievement(achievement: str, pass_to_account: bool = True) -> None:
|
||||
|
||||
"""(internal)"""
|
||||
return None
|
||||
|
||||
|
||||
def reset_achievements() -> None:
|
||||
|
||||
"""(internal)"""
|
||||
return None
|
||||
|
||||
|
||||
def restore_purchases() -> None:
|
||||
|
||||
"""(internal)"""
|
||||
return None
|
||||
|
||||
|
||||
def run_transactions() -> None:
|
||||
|
||||
"""(internal)"""
|
||||
return None
|
||||
|
||||
|
||||
def sign_in_v1(account_type: str) -> None:
|
||||
|
||||
"""(internal)
|
||||
|
||||
Category: General Utility Functions
|
||||
|
|
@ -207,6 +238,7 @@ def sign_in_v1(account_type: str) -> None:
|
|||
|
||||
|
||||
def sign_out_v1(v2_embedded: bool = False) -> None:
|
||||
|
||||
"""(internal)
|
||||
|
||||
Category: General Utility Functions
|
||||
|
|
@ -214,17 +246,20 @@ def sign_out_v1(v2_embedded: bool = False) -> None:
|
|||
return None
|
||||
|
||||
|
||||
def submit_score(game: str,
|
||||
config: str,
|
||||
name: Any,
|
||||
score: int | None,
|
||||
callback: Callable,
|
||||
friend_callback: Callable | None,
|
||||
order: str = 'increasing',
|
||||
tournament_id: str | None = None,
|
||||
score_type: str = 'points',
|
||||
campaign: str | None = None,
|
||||
level: str | None = None) -> None:
|
||||
def submit_score(
|
||||
game: str,
|
||||
config: str,
|
||||
name: Any,
|
||||
score: int | None,
|
||||
callback: Callable,
|
||||
friend_callback: Callable | None,
|
||||
order: str = 'increasing',
|
||||
tournament_id: str | None = None,
|
||||
score_type: str = 'points',
|
||||
campaign: str | None = None,
|
||||
level: str | None = None,
|
||||
) -> None:
|
||||
|
||||
"""(internal)
|
||||
|
||||
Submit a score to the server; callback will be called with the results.
|
||||
|
|
@ -235,7 +270,9 @@ def submit_score(game: str,
|
|||
return None
|
||||
|
||||
|
||||
def tournament_query(callback: Callable[[dict | None], None],
|
||||
args: dict) -> None:
|
||||
def tournament_query(
|
||||
callback: Callable[[dict | None], None], args: dict
|
||||
) -> None:
|
||||
|
||||
"""(internal)"""
|
||||
return None
|
||||
|
|
|
|||
395
dist/ba_data/python/ba/__init__.py
vendored
395
dist/ba_data/python/ba/__init__.py
vendored
|
|
@ -9,15 +9,62 @@ In some specific cases you may need to pull in individual submodules instead.
|
|||
# pylint: disable=redefined-builtin
|
||||
|
||||
from _ba import (
|
||||
CollideModel, Context, ContextCall, Data, InputDevice, Material, Model,
|
||||
Node, SessionPlayer, Sound, Texture, Timer, Vec3, Widget, buttonwidget,
|
||||
camerashake, checkboxwidget, columnwidget, containerwidget, do_once,
|
||||
emitfx, getactivity, getcollidemodel, getmodel, getnodes, getsession,
|
||||
getsound, gettexture, hscrollwidget, imagewidget, newactivity, newnode,
|
||||
playsound, printnodes, 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, getdata, in_logic_thread)
|
||||
CollideModel,
|
||||
Context,
|
||||
ContextCall,
|
||||
Data,
|
||||
InputDevice,
|
||||
Material,
|
||||
Model,
|
||||
Node,
|
||||
SessionPlayer,
|
||||
Sound,
|
||||
Texture,
|
||||
Timer,
|
||||
Vec3,
|
||||
Widget,
|
||||
buttonwidget,
|
||||
camerashake,
|
||||
checkboxwidget,
|
||||
columnwidget,
|
||||
containerwidget,
|
||||
do_once,
|
||||
emitfx,
|
||||
getactivity,
|
||||
getcollidemodel,
|
||||
getmodel,
|
||||
getnodes,
|
||||
getsession,
|
||||
getsound,
|
||||
gettexture,
|
||||
hscrollwidget,
|
||||
imagewidget,
|
||||
newactivity,
|
||||
newnode,
|
||||
playsound,
|
||||
printnodes,
|
||||
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,
|
||||
getdata,
|
||||
in_logic_thread,
|
||||
)
|
||||
from ba._accountv2 import AccountV2Handle
|
||||
from ba._activity import Activity
|
||||
from ba._plugin import PotentialPlugin, Plugin, PluginSubsystem
|
||||
from ba._actor import Actor
|
||||
|
|
@ -27,21 +74,50 @@ from ba._app import App
|
|||
from ba._cloud import CloudSubsystem
|
||||
from ba._coopgame import CoopGameActivity
|
||||
from ba._coopsession import CoopSession
|
||||
from ba._dependency import (Dependency, DependencyComponent, DependencySet,
|
||||
AssetPackage)
|
||||
from ba._generated.enums import (TimeType, Permission, TimeFormat, SpecialChar,
|
||||
InputType, UIScale)
|
||||
from ba._dependency import (
|
||||
Dependency,
|
||||
DependencyComponent,
|
||||
DependencySet,
|
||||
AssetPackage,
|
||||
)
|
||||
from ba._generated.enums import (
|
||||
TimeType,
|
||||
Permission,
|
||||
TimeFormat,
|
||||
SpecialChar,
|
||||
InputType,
|
||||
UIScale,
|
||||
)
|
||||
from ba._error import (
|
||||
print_exception, print_error, ContextError, NotFoundError,
|
||||
PlayerNotFoundError, SessionPlayerNotFoundError, NodeNotFoundError,
|
||||
ActorNotFoundError, InputDeviceNotFoundError, WidgetNotFoundError,
|
||||
ActivityNotFoundError, TeamNotFoundError, SessionTeamNotFoundError,
|
||||
SessionNotFoundError, DelegateNotFoundError, DependencyError)
|
||||
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 GameResults
|
||||
from ba._settings import (Setting, IntSetting, FloatSetting, ChoiceSetting,
|
||||
BoolSetting, IntChoiceSetting, FloatChoiceSetting)
|
||||
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
|
||||
|
|
@ -57,23 +133,53 @@ from ba._appconfig import AppConfig
|
|||
from ba._appdelegate import AppDelegate
|
||||
from ba._apputils import is_browser_likely_available, garbage_collect
|
||||
from ba._campaign import Campaign
|
||||
from ba._gameutils import (GameTip, animate, animate_array, show_damage_count,
|
||||
timestring, cameraflash)
|
||||
from ba._general import (WeakCall, Call, existing, Existable,
|
||||
verify_object_death, storagename, getclass)
|
||||
from ba._gameutils import (
|
||||
GameTip,
|
||||
animate,
|
||||
animate_array,
|
||||
show_damage_count,
|
||||
timestring,
|
||||
cameraflash,
|
||||
)
|
||||
from ba._general import (
|
||||
WeakCall,
|
||||
Call,
|
||||
existing,
|
||||
Existable,
|
||||
verify_object_death,
|
||||
storagename,
|
||||
getclass,
|
||||
)
|
||||
from ba._keyboard import Keyboard
|
||||
from ba._level import Level
|
||||
from ba._lobby import Lobby, Chooser
|
||||
from ba._math import normalized_color, is_point_in_box, vec3validate
|
||||
from ba._meta import MetadataSubsystem
|
||||
from ba._messages import (UNHANDLED, OutOfBoundsMessage, DeathType, DieMessage,
|
||||
PlayerDiedMessage, StandMessage, PickUpMessage,
|
||||
DropMessage, PickedUpMessage, DroppedMessage,
|
||||
ShouldShatterMessage, ImpactDamageMessage,
|
||||
FreezeMessage, ThawMessage, HitMessage,
|
||||
CelebrateMessage)
|
||||
from ba._music import (setmusic, MusicPlayer, MusicType, MusicPlayMode,
|
||||
MusicSubsystem)
|
||||
from ba._messages import (
|
||||
UNHANDLED,
|
||||
OutOfBoundsMessage,
|
||||
DeathType,
|
||||
DieMessage,
|
||||
PlayerDiedMessage,
|
||||
StandMessage,
|
||||
PickUpMessage,
|
||||
DropMessage,
|
||||
PickedUpMessage,
|
||||
DroppedMessage,
|
||||
ShouldShatterMessage,
|
||||
ImpactDamageMessage,
|
||||
FreezeMessage,
|
||||
ThawMessage,
|
||||
HitMessage,
|
||||
CelebrateMessage,
|
||||
)
|
||||
from ba._music import (
|
||||
setmusic,
|
||||
MusicPlayer,
|
||||
MusicType,
|
||||
MusicPlayMode,
|
||||
MusicSubsystem,
|
||||
)
|
||||
from ba._powerup import PowerupMessage, PowerupAcceptMessage
|
||||
from ba._multiteamsession import MultiTeamSession
|
||||
from ba.ui import Window, UIController, uicleanupcheck
|
||||
|
|
@ -82,47 +188,186 @@ from ba._collision import Collision, getcollision
|
|||
app: App
|
||||
|
||||
__all__ = [
|
||||
'Achievement', 'AchievementSubsystem', 'Activity', 'ActivityNotFoundError',
|
||||
'Actor', 'ActorNotFoundError', 'animate', 'animate_array', 'app', 'App',
|
||||
'AppConfig', 'AppDelegate', 'AssetPackage', 'BoolSetting', 'buttonwidget',
|
||||
'Call', 'cameraflash', 'camerashake', 'Campaign', 'CelebrateMessage',
|
||||
'charstr', 'checkboxwidget', 'ChoiceSetting', 'Chooser',
|
||||
'clipboard_get_text', 'clipboard_has_text', 'clipboard_is_supported',
|
||||
'clipboard_set_text', 'CollideModel', 'Collision', 'columnwidget',
|
||||
'containerwidget', 'Context', 'ContextCall', 'ContextError',
|
||||
'CloudSubsystem', 'CoopGameActivity', 'CoopSession', 'Data', 'DeathType',
|
||||
'DelegateNotFoundError', 'Dependency', 'DependencyComponent',
|
||||
'DependencyError', 'DependencySet', 'DieMessage', 'do_once', 'DropMessage',
|
||||
'DroppedMessage', 'DualTeamSession', 'emitfx', 'EmptyPlayer', 'EmptyTeam',
|
||||
'Existable', 'existing', 'FloatChoiceSetting', 'FloatSetting',
|
||||
'FreeForAllSession', 'FreezeMessage', 'GameActivity', 'GameResults',
|
||||
'GameTip', 'garbage_collect', 'getactivity', 'getclass', 'getcollidemodel',
|
||||
'getcollision', 'getdata', 'getmaps', 'getmodel', 'getnodes', 'getsession',
|
||||
'getsound', 'gettexture', 'HitMessage', 'hscrollwidget', 'imagewidget',
|
||||
'ImpactDamageMessage', 'in_logic_thread', 'InputDevice',
|
||||
'InputDeviceNotFoundError', 'InputType', 'IntChoiceSetting', 'IntSetting',
|
||||
'is_browser_likely_available', 'is_point_in_box', 'Keyboard',
|
||||
'LanguageSubsystem', 'Level', 'Lobby', 'Lstr', 'Map', 'Material',
|
||||
'MetadataSubsystem', 'Model', 'MultiTeamSession', 'MusicPlayer',
|
||||
'MusicPlayMode', 'MusicSubsystem', 'MusicType', 'newactivity', 'newnode',
|
||||
'Node', 'NodeActor', 'NodeNotFoundError', 'normalized_color',
|
||||
'NotFoundError', 'open_url', 'OutOfBoundsMessage', 'Permission',
|
||||
'PickedUpMessage', 'PickUpMessage', 'Player', 'PlayerDiedMessage',
|
||||
'PlayerInfo', 'PlayerNotFoundError', 'PlayerRecord', 'PlayerScoredMessage',
|
||||
'playsound', 'Plugin', 'PluginSubsystem', 'PotentialPlugin',
|
||||
'PowerupAcceptMessage', 'PowerupMessage', 'print_error', 'print_exception',
|
||||
'printnodes', 'printobjects', 'pushcall', 'quit', 'rowwidget', 'safecolor',
|
||||
'ScoreConfig', 'ScoreType', 'screenmessage', 'scrollwidget',
|
||||
'ServerController', 'Session', 'SessionNotFoundError', 'SessionPlayer',
|
||||
'SessionPlayerNotFoundError', 'SessionTeam', 'SessionTeamNotFoundError',
|
||||
'set_analytics_screen', 'setmusic', 'Setting', 'ShouldShatterMessage',
|
||||
'show_damage_count', 'Sound', 'SpecialChar', 'StandLocation',
|
||||
'StandMessage', 'Stats', 'storagename', 'Team', 'TeamGameActivity',
|
||||
'TeamNotFoundError', 'Texture', 'textwidget', 'ThawMessage', 'time',
|
||||
'TimeFormat', 'Timer', 'timer', 'timestring', 'TimeType', 'uicleanupcheck',
|
||||
'UIController', 'UIScale', 'UISubsystem', 'UNHANDLED', 'Vec3',
|
||||
'vec3validate', 'verify_object_death', 'WeakCall', 'Widget', 'widget',
|
||||
'WidgetNotFoundError', 'Window'
|
||||
'AccountV2Handle',
|
||||
'Achievement',
|
||||
'AchievementSubsystem',
|
||||
'Activity',
|
||||
'ActivityNotFoundError',
|
||||
'Actor',
|
||||
'ActorNotFoundError',
|
||||
'animate',
|
||||
'animate_array',
|
||||
'app',
|
||||
'App',
|
||||
'AppConfig',
|
||||
'AppDelegate',
|
||||
'AssetPackage',
|
||||
'BoolSetting',
|
||||
'buttonwidget',
|
||||
'Call',
|
||||
'cameraflash',
|
||||
'camerashake',
|
||||
'Campaign',
|
||||
'CelebrateMessage',
|
||||
'charstr',
|
||||
'checkboxwidget',
|
||||
'ChoiceSetting',
|
||||
'Chooser',
|
||||
'clipboard_get_text',
|
||||
'clipboard_has_text',
|
||||
'clipboard_is_supported',
|
||||
'clipboard_set_text',
|
||||
'CollideModel',
|
||||
'Collision',
|
||||
'columnwidget',
|
||||
'containerwidget',
|
||||
'Context',
|
||||
'ContextCall',
|
||||
'ContextError',
|
||||
'CloudSubsystem',
|
||||
'CoopGameActivity',
|
||||
'CoopSession',
|
||||
'Data',
|
||||
'DeathType',
|
||||
'DelegateNotFoundError',
|
||||
'Dependency',
|
||||
'DependencyComponent',
|
||||
'DependencyError',
|
||||
'DependencySet',
|
||||
'DieMessage',
|
||||
'do_once',
|
||||
'DropMessage',
|
||||
'DroppedMessage',
|
||||
'DualTeamSession',
|
||||
'emitfx',
|
||||
'EmptyPlayer',
|
||||
'EmptyTeam',
|
||||
'Existable',
|
||||
'existing',
|
||||
'FloatChoiceSetting',
|
||||
'FloatSetting',
|
||||
'FreeForAllSession',
|
||||
'FreezeMessage',
|
||||
'GameActivity',
|
||||
'GameResults',
|
||||
'GameTip',
|
||||
'garbage_collect',
|
||||
'getactivity',
|
||||
'getclass',
|
||||
'getcollidemodel',
|
||||
'getcollision',
|
||||
'getdata',
|
||||
'getmaps',
|
||||
'getmodel',
|
||||
'getnodes',
|
||||
'getsession',
|
||||
'getsound',
|
||||
'gettexture',
|
||||
'HitMessage',
|
||||
'hscrollwidget',
|
||||
'imagewidget',
|
||||
'ImpactDamageMessage',
|
||||
'in_logic_thread',
|
||||
'InputDevice',
|
||||
'InputDeviceNotFoundError',
|
||||
'InputType',
|
||||
'IntChoiceSetting',
|
||||
'IntSetting',
|
||||
'is_browser_likely_available',
|
||||
'is_point_in_box',
|
||||
'Keyboard',
|
||||
'LanguageSubsystem',
|
||||
'Level',
|
||||
'Lobby',
|
||||
'Lstr',
|
||||
'Map',
|
||||
'Material',
|
||||
'MetadataSubsystem',
|
||||
'Model',
|
||||
'MultiTeamSession',
|
||||
'MusicPlayer',
|
||||
'MusicPlayMode',
|
||||
'MusicSubsystem',
|
||||
'MusicType',
|
||||
'newactivity',
|
||||
'newnode',
|
||||
'Node',
|
||||
'NodeActor',
|
||||
'NodeNotFoundError',
|
||||
'normalized_color',
|
||||
'NotFoundError',
|
||||
'open_url',
|
||||
'OutOfBoundsMessage',
|
||||
'Permission',
|
||||
'PickedUpMessage',
|
||||
'PickUpMessage',
|
||||
'Player',
|
||||
'PlayerDiedMessage',
|
||||
'PlayerInfo',
|
||||
'PlayerNotFoundError',
|
||||
'PlayerRecord',
|
||||
'PlayerScoredMessage',
|
||||
'playsound',
|
||||
'Plugin',
|
||||
'PluginSubsystem',
|
||||
'PotentialPlugin',
|
||||
'PowerupAcceptMessage',
|
||||
'PowerupMessage',
|
||||
'print_error',
|
||||
'print_exception',
|
||||
'printnodes',
|
||||
'printobjects',
|
||||
'pushcall',
|
||||
'quit',
|
||||
'rowwidget',
|
||||
'safecolor',
|
||||
'ScoreConfig',
|
||||
'ScoreType',
|
||||
'screenmessage',
|
||||
'scrollwidget',
|
||||
'ServerController',
|
||||
'Session',
|
||||
'SessionNotFoundError',
|
||||
'SessionPlayer',
|
||||
'SessionPlayerNotFoundError',
|
||||
'SessionTeam',
|
||||
'SessionTeamNotFoundError',
|
||||
'set_analytics_screen',
|
||||
'setmusic',
|
||||
'Setting',
|
||||
'ShouldShatterMessage',
|
||||
'show_damage_count',
|
||||
'Sound',
|
||||
'SpecialChar',
|
||||
'StandLocation',
|
||||
'StandMessage',
|
||||
'Stats',
|
||||
'storagename',
|
||||
'Team',
|
||||
'TeamGameActivity',
|
||||
'TeamNotFoundError',
|
||||
'Texture',
|
||||
'textwidget',
|
||||
'ThawMessage',
|
||||
'time',
|
||||
'TimeFormat',
|
||||
'Timer',
|
||||
'timer',
|
||||
'timestring',
|
||||
'TimeType',
|
||||
'uicleanupcheck',
|
||||
'UIController',
|
||||
'UIScale',
|
||||
'UISubsystem',
|
||||
'UNHANDLED',
|
||||
'Vec3',
|
||||
'vec3validate',
|
||||
'verify_object_death',
|
||||
'WeakCall',
|
||||
'Widget',
|
||||
'widget',
|
||||
'WidgetNotFoundError',
|
||||
'Window',
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -135,10 +380,12 @@ def _simplify_module_names() -> None:
|
|||
# so let's make an exception for it.
|
||||
if os.environ.get('BA_DOCS_GENERATION', '0') != '1':
|
||||
from efro.util import set_canonical_module
|
||||
|
||||
globs = globals()
|
||||
set_canonical_module(
|
||||
module_globals=globs,
|
||||
names=[n for n in globs.keys() if not n.startswith('_')])
|
||||
names=[n for n in globs.keys() if not n.startswith('_')],
|
||||
)
|
||||
|
||||
|
||||
_simplify_module_names()
|
||||
|
|
|
|||
180
dist/ba_data/python/ba/_accountv1.py
vendored
180
dist/ba_data/python/ba/_accountv1.py
vendored
|
|
@ -40,8 +40,10 @@ class AccountV1Subsystem:
|
|||
|
||||
# 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':
|
||||
if (
|
||||
_ba.app.headless_mode
|
||||
or _ba.app.config.get('Auto Account State') == 'Local'
|
||||
):
|
||||
_internal.sign_in_v1('Local')
|
||||
|
||||
_ba.pushcall(do_auto_sign_in)
|
||||
|
|
@ -60,9 +62,14 @@ class AccountV1Subsystem:
|
|||
(internal)
|
||||
"""
|
||||
from ba._language import Lstr
|
||||
_ba.screenmessage(Lstr(resource='getTicketsWindow.receivedTicketsText',
|
||||
subs=[('${COUNT}', str(count))]),
|
||||
color=(0, 1, 0))
|
||||
|
||||
_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:
|
||||
|
|
@ -73,9 +80,9 @@ class AccountV1Subsystem:
|
|||
"""(internal)"""
|
||||
return self.league_rank_cache.get('info', None)
|
||||
|
||||
def get_league_rank_points(self,
|
||||
data: dict[str, Any] | None,
|
||||
subset: str | None = None) -> int:
|
||||
def get_league_rank_points(
|
||||
self, data: dict[str, Any] | None, subset: str | None = None
|
||||
) -> int:
|
||||
"""(internal)"""
|
||||
if data is None:
|
||||
return 0
|
||||
|
|
@ -90,15 +97,23 @@ class AccountV1Subsystem:
|
|||
if ach.complete:
|
||||
total_ach_value += ach.power_ranking_value
|
||||
|
||||
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'])
|
||||
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'])
|
||||
val: int = (
|
||||
data['t0a']
|
||||
+ data['t0b']
|
||||
+ data['t1']
|
||||
+ data['t2']
|
||||
+ data['t3']
|
||||
+ data['t4']
|
||||
)
|
||||
assert isinstance(val, int)
|
||||
return val
|
||||
if subset == 'trophies':
|
||||
|
|
@ -108,41 +123,54 @@ class AccountV1Subsystem:
|
|||
raise ValueError('invalid subset value: ' + str(subset))
|
||||
|
||||
if data['p']:
|
||||
pro_mult = 1.0 + float(
|
||||
_internal.get_v1_account_misc_read_val('proPowerRankingBoost',
|
||||
0.0)) * 0.01
|
||||
pro_mult = (
|
||||
1.0
|
||||
+ float(
|
||||
_internal.get_v1_account_misc_read_val(
|
||||
'proPowerRankingBoost', 0.0
|
||||
)
|
||||
)
|
||||
* 0.01
|
||||
)
|
||||
else:
|
||||
pro_mult = 1.0
|
||||
|
||||
# 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)
|
||||
(total_ach_value + trophies_total)
|
||||
* (data['act'] if data['act'] is not None else 1.0)
|
||||
* pro_mult
|
||||
)
|
||||
|
||||
def cache_tournament_info(self, info: Any) -> None:
|
||||
"""(internal)"""
|
||||
from ba._generated.enums import TimeType, TimeFormat
|
||||
|
||||
for entry in info:
|
||||
cache_entry = self.tournament_info[entry['tournamentID']] = (
|
||||
copy.deepcopy(entry))
|
||||
cache_entry = self.tournament_info[
|
||||
entry['tournamentID']
|
||||
] = copy.deepcopy(entry)
|
||||
|
||||
# Also store the time we received this, so we can adjust
|
||||
# time-remaining values/etc.
|
||||
cache_entry['timeReceived'] = _ba.time(TimeType.REAL,
|
||||
TimeFormat.MILLISECONDS)
|
||||
cache_entry['timeReceived'] = _ba.time(
|
||||
TimeType.REAL, TimeFormat.MILLISECONDS
|
||||
)
|
||||
cache_entry['valid'] = True
|
||||
|
||||
def get_purchased_icons(self) -> list[str]:
|
||||
"""(internal)"""
|
||||
# pylint: disable=cyclic-import
|
||||
from ba import _store
|
||||
|
||||
if _internal.get_v1_account_state() != 'signed_in':
|
||||
return []
|
||||
icons = []
|
||||
store_items = _store.get_store_items()
|
||||
for item_name, item in list(store_items.items()):
|
||||
if item_name.startswith('icons.') and _internal.get_purchased(
|
||||
item_name):
|
||||
item_name
|
||||
):
|
||||
icons.append(item['icon'])
|
||||
return icons
|
||||
|
||||
|
|
@ -160,23 +188,28 @@ class AccountV1Subsystem:
|
|||
# If the short version of our account name currently cant be
|
||||
# displayed by the game, cancel.
|
||||
if not _ba.have_chars(
|
||||
_internal.get_v1_account_display_string(full=False)):
|
||||
_internal.get_v1_account_display_string(full=False)
|
||||
):
|
||||
return
|
||||
|
||||
config = _ba.app.config
|
||||
if ('Player Profiles' not in config
|
||||
or '__account__' not in config['Player Profiles']):
|
||||
if (
|
||||
'Player Profiles' not in config
|
||||
or '__account__' not in config['Player Profiles']
|
||||
):
|
||||
|
||||
# Create a spaz with a nice default purply color.
|
||||
_internal.add_transaction({
|
||||
'type': 'ADD_PLAYER_PROFILE',
|
||||
'name': '__account__',
|
||||
'profile': {
|
||||
'character': 'Spaz',
|
||||
'color': [0.5, 0.25, 1.0],
|
||||
'highlight': [0.5, 0.25, 1.0]
|
||||
_internal.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],
|
||||
},
|
||||
}
|
||||
})
|
||||
)
|
||||
_internal.run_transactions()
|
||||
|
||||
def have_pro(self) -> bool:
|
||||
|
|
@ -188,7 +221,8 @@ class AccountV1Subsystem:
|
|||
_internal.get_purchased('upgrades.pro')
|
||||
or _internal.get_purchased('static.pro')
|
||||
or _internal.get_purchased('static.pro_sale')
|
||||
or 'ballistica' + 'core' == _ba.appname())
|
||||
or 'ballistica' + 'core' == _ba.appname()
|
||||
)
|
||||
|
||||
def have_pro_options(self) -> bool:
|
||||
"""Return whether pro-options are present.
|
||||
|
|
@ -199,25 +233,34 @@ class AccountV1Subsystem:
|
|||
|
||||
# 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.
|
||||
# or also if we've been grandfathered in
|
||||
# or are using ballistica-core builds.
|
||||
return self.have_pro() or bool(
|
||||
_internal.get_v1_account_misc_read_val_2('proOptionsUnlocked',
|
||||
False)
|
||||
or _ba.app.config.get('lc14292', 0) > 1)
|
||||
_internal.get_v1_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._generated.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):
|
||||
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.screenmessage(
|
||||
Lstr(
|
||||
resource='updatingAccountText',
|
||||
fallback_resource='purchasingText',
|
||||
),
|
||||
color=(0, 1, 0),
|
||||
)
|
||||
_ba.playsound(_ba.getsound('click01'))
|
||||
|
||||
def on_account_state_changed(self) -> None:
|
||||
|
|
@ -225,16 +268,21 @@ class AccountV1Subsystem:
|
|||
from ba._language import Lstr
|
||||
|
||||
# Run any pending promo codes we had queued up while not signed in.
|
||||
if _internal.get_v1_account_state(
|
||||
) == 'signed_in' and self.pending_promo_codes:
|
||||
if (
|
||||
_internal.get_v1_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))
|
||||
_internal.add_transaction({
|
||||
'type': 'PROMO_CODE',
|
||||
'expire_time': time.time() + 5,
|
||||
'code': code
|
||||
})
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='submittingPromoCodeText'), color=(0, 1, 0)
|
||||
)
|
||||
_internal.add_transaction(
|
||||
{
|
||||
'type': 'PROMO_CODE',
|
||||
'expire_time': time.time() + 5,
|
||||
'code': code,
|
||||
}
|
||||
)
|
||||
_internal.run_transactions()
|
||||
self.pending_promo_codes = []
|
||||
|
||||
|
|
@ -254,18 +302,18 @@ class AccountV1Subsystem:
|
|||
# If we're still not signed in and have pending codes,
|
||||
# inform the user that they need to sign in to use them.
|
||||
if self.pending_promo_codes:
|
||||
_ba.screenmessage(Lstr(resource='signInForPromoCodeText'),
|
||||
color=(1, 0, 0))
|
||||
_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))
|
||||
_internal.add_transaction({
|
||||
'type': 'PROMO_CODE',
|
||||
'expire_time': time.time() + 5,
|
||||
'code': code
|
||||
})
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='submittingPromoCodeText'), color=(0, 1, 0)
|
||||
)
|
||||
_internal.add_transaction(
|
||||
{'type': 'PROMO_CODE', 'expire_time': time.time() + 5, 'code': code}
|
||||
)
|
||||
_internal.run_transactions()
|
||||
|
|
|
|||
32
dist/ba_data/python/ba/_accountv2.py
vendored
32
dist/ba_data/python/ba/_accountv2.py
vendored
|
|
@ -9,7 +9,7 @@ from typing import TYPE_CHECKING
|
|||
import _ba
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
from typing import Any
|
||||
|
||||
|
||||
class AccountV2Subsystem:
|
||||
|
|
@ -55,8 +55,9 @@ class AccountV2Subsystem:
|
|||
"""Internal - should be overridden by subclass."""
|
||||
return None
|
||||
|
||||
def on_primary_account_changed(self,
|
||||
account: AccountV2Handle | None) -> None:
|
||||
def on_primary_account_changed(
|
||||
self, account: AccountV2Handle | None
|
||||
) -> None:
|
||||
"""Callback run after the primary account changes.
|
||||
|
||||
Will be called with None on log-outs or when new credentials
|
||||
|
|
@ -70,13 +71,17 @@ class AccountV2Subsystem:
|
|||
# informed when that process completes.
|
||||
if account.workspaceid is not None:
|
||||
assert account.workspacename is not None
|
||||
if (not self._initial_login_completed
|
||||
and not self._kicked_off_workspace_load):
|
||||
if (
|
||||
not self._initial_login_completed
|
||||
and not self._kicked_off_workspace_load
|
||||
):
|
||||
self._kicked_off_workspace_load = True
|
||||
_ba.app.workspaces.set_active_workspace(
|
||||
account=account,
|
||||
workspaceid=account.workspaceid,
|
||||
workspacename=account.workspacename,
|
||||
on_completed=self._on_set_active_workspace_completed)
|
||||
on_completed=self._on_set_active_workspace_completed,
|
||||
)
|
||||
else:
|
||||
# Don't activate workspaces if we've already told the game
|
||||
# that initial-log-in is done or if we've already kicked
|
||||
|
|
@ -84,7 +89,8 @@ class AccountV2Subsystem:
|
|||
_ba.screenmessage(
|
||||
f'\'{account.workspacename}\''
|
||||
f' will be activated at next app launch.',
|
||||
color=(1, 1, 0))
|
||||
color=(1, 1, 0),
|
||||
)
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
return
|
||||
|
||||
|
|
@ -111,10 +117,20 @@ class AccountV2Subsystem:
|
|||
|
||||
|
||||
class AccountV2Handle:
|
||||
"""Handle for interacting with a v2 account."""
|
||||
"""Handle for interacting with a V2 account.
|
||||
|
||||
This class supports the 'with' statement, which is how it is
|
||||
used with some operations such as cloud messaging.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.tag = '?'
|
||||
|
||||
self.workspacename: str | None = None
|
||||
self.workspaceid: str | None = None
|
||||
|
||||
def __enter__(self) -> None:
|
||||
"""Support for "with" statement."""
|
||||
|
||||
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any:
|
||||
"""Support for "with" statement."""
|
||||
|
|
|
|||
1253
dist/ba_data/python/ba/_achievement.py
vendored
1253
dist/ba_data/python/ba/_achievement.py
vendored
File diff suppressed because it is too large
Load diff
114
dist/ba_data/python/ba/_activity.py
vendored
114
dist/ba_data/python/ba/_activity.py
vendored
|
|
@ -9,8 +9,12 @@ from typing import TYPE_CHECKING, Generic, TypeVar
|
|||
import _ba
|
||||
from ba._team import Team
|
||||
from ba._player import Player
|
||||
from ba._error import (print_exception, SessionTeamNotFoundError,
|
||||
SessionPlayerNotFoundError, NodeNotFoundError)
|
||||
from ba._error import (
|
||||
print_exception,
|
||||
SessionTeamNotFoundError,
|
||||
SessionPlayerNotFoundError,
|
||||
NodeNotFoundError,
|
||||
)
|
||||
from ba._dependency import DependencyComponent
|
||||
from ba._general import Call, verify_object_death
|
||||
from ba._messages import UNHANDLED
|
||||
|
|
@ -199,8 +203,11 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
session = self._session()
|
||||
if session is not None:
|
||||
_ba.pushcall(
|
||||
Call(session.transitioning_out_activity_was_freed,
|
||||
self.can_show_ad_on_death))
|
||||
Call(
|
||||
session.transitioning_out_activity_was_freed,
|
||||
self.can_show_ad_on_death,
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def globalsnode(self) -> ba.Node:
|
||||
|
|
@ -220,6 +227,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
"""
|
||||
if self._stats is None:
|
||||
from ba._error import NotFoundError
|
||||
|
||||
raise NotFoundError()
|
||||
return self._stats
|
||||
|
||||
|
|
@ -285,7 +293,8 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
5.0,
|
||||
Call(self._check_activity_death, ref, [0]),
|
||||
repeat=True,
|
||||
timetype=TimeType.REAL)
|
||||
timetype=TimeType.REAL,
|
||||
)
|
||||
|
||||
# Run _expire in an empty context; nothing should be happening in
|
||||
# there except deleting things which requires no context.
|
||||
|
|
@ -296,8 +305,9 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
with _ba.Context('empty'):
|
||||
self._expire()
|
||||
else:
|
||||
raise RuntimeError(f'destroy() called when'
|
||||
f' already expired for {self}')
|
||||
raise RuntimeError(
|
||||
f'destroy() called when' f' already expired for {self}'
|
||||
)
|
||||
|
||||
def retain_actor(self, actor: ba.Actor) -> None:
|
||||
"""Add a strong-reference to a ba.Actor to this Activity.
|
||||
|
|
@ -308,6 +318,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
"""
|
||||
if __debug__:
|
||||
from ba._actor import Actor
|
||||
|
||||
assert isinstance(actor, Actor)
|
||||
self._actor_refs.append(actor)
|
||||
|
||||
|
|
@ -318,6 +329,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
"""
|
||||
if __debug__:
|
||||
from ba._actor import Actor
|
||||
|
||||
assert isinstance(actor, Actor)
|
||||
self._actor_weak_refs.append(weakref.ref(actor))
|
||||
|
||||
|
|
@ -330,6 +342,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
session = self._session()
|
||||
if session is None:
|
||||
from ba._error import SessionNotFoundError
|
||||
|
||||
raise SessionNotFoundError()
|
||||
return session
|
||||
|
||||
|
|
@ -381,7 +394,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
|
||||
def has_transitioned_in(self) -> bool:
|
||||
"""Return whether ba.Activity.on_transition_in()
|
||||
has been called."""
|
||||
has been called."""
|
||||
return self._has_transitioned_in
|
||||
|
||||
def has_begun(self) -> bool:
|
||||
|
|
@ -425,7 +438,8 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
if self.inherits_vr_overlay_center and prev_globals is not None:
|
||||
glb.vr_overlay_center = prev_globals.vr_overlay_center
|
||||
glb.vr_overlay_center_enabled = (
|
||||
prev_globals.vr_overlay_center_enabled)
|
||||
prev_globals.vr_overlay_center_enabled
|
||||
)
|
||||
|
||||
# If they want to inherit tint from the previous self.
|
||||
if self.inherits_tint and prev_globals is not None:
|
||||
|
|
@ -435,9 +449,9 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
|
||||
# Start pruning our various things periodically.
|
||||
self._prune_dead_actors()
|
||||
self._prune_dead_actors_timer = _ba.Timer(5.17,
|
||||
self._prune_dead_actors,
|
||||
repeat=True)
|
||||
self._prune_dead_actors_timer = _ba.Timer(
|
||||
5.17, self._prune_dead_actors, repeat=True
|
||||
)
|
||||
|
||||
_ba.timer(13.3, self._prune_delay_deletes, repeat=True)
|
||||
|
||||
|
|
@ -491,10 +505,9 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
# activity launch; just wanna be sure that is intentional.
|
||||
self.on_begin()
|
||||
|
||||
def end(self,
|
||||
results: Any = None,
|
||||
delay: float = 0.0,
|
||||
force: bool = False) -> None:
|
||||
def end(
|
||||
self, results: Any = None, delay: float = 0.0, force: bool = False
|
||||
) -> None:
|
||||
"""Commences Activity shutdown and delivers results to the ba.Session.
|
||||
|
||||
'delay' is the time delay before the Activity actually ends
|
||||
|
|
@ -543,7 +556,8 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
sessionplayer.setactivity(self)
|
||||
with _ba.Context(self):
|
||||
sessionplayer.activityplayer = player = self.create_player(
|
||||
sessionplayer)
|
||||
sessionplayer
|
||||
)
|
||||
player.postinit(sessionplayer)
|
||||
|
||||
assert player not in team.players
|
||||
|
|
@ -654,7 +668,8 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
self._teams_that_left.append(weakref.ref(team))
|
||||
|
||||
def _reset_session_player_for_no_activity(
|
||||
self, sessionplayer: ba.SessionPlayer) -> None:
|
||||
self, sessionplayer: ba.SessionPlayer
|
||||
) -> None:
|
||||
|
||||
# Let's be extra-defensive here: killing a node/input-call/etc
|
||||
# could trigger user-code resulting in errors, but we would still
|
||||
|
|
@ -664,13 +679,15 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
except Exception:
|
||||
print_exception(
|
||||
f'Error resetting SessionPlayer node on {sessionplayer}'
|
||||
f' for {self}.')
|
||||
f' for {self}.'
|
||||
)
|
||||
try:
|
||||
sessionplayer.resetinput()
|
||||
except Exception:
|
||||
print_exception(
|
||||
f'Error resetting SessionPlayer input on {sessionplayer}'
|
||||
f' for {self}.')
|
||||
f' for {self}.'
|
||||
)
|
||||
|
||||
# These should never fail I think...
|
||||
sessionplayer.setactivity(None)
|
||||
|
|
@ -691,21 +708,26 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
self._playertype = type(self).__orig_bases__[-1].__args__[0]
|
||||
if not isinstance(self._playertype, type):
|
||||
self._playertype = Player
|
||||
print(f'ERROR: {type(self)} was not passed a Player'
|
||||
f' type argument; please explicitly pass ba.Player'
|
||||
f' if you do not want to override it.')
|
||||
print(
|
||||
f'ERROR: {type(self)} was not passed a Player'
|
||||
f' type argument; please explicitly pass ba.Player'
|
||||
f' if you do not want to override it.'
|
||||
)
|
||||
self._teamtype = type(self).__orig_bases__[-1].__args__[1]
|
||||
if not isinstance(self._teamtype, type):
|
||||
self._teamtype = Team
|
||||
print(f'ERROR: {type(self)} was not passed a Team'
|
||||
f' type argument; please explicitly pass ba.Team'
|
||||
f' if you do not want to override it.')
|
||||
print(
|
||||
f'ERROR: {type(self)} was not passed a Team'
|
||||
f' type argument; please explicitly pass ba.Team'
|
||||
f' if you do not want to override it.'
|
||||
)
|
||||
assert issubclass(self._playertype, Player)
|
||||
assert issubclass(self._teamtype, Team)
|
||||
|
||||
@classmethod
|
||||
def _check_activity_death(cls, activity_ref: weakref.ref[Activity],
|
||||
counter: list[int]) -> None:
|
||||
def _check_activity_death(
|
||||
cls, activity_ref: weakref.ref[Activity], counter: list[int]
|
||||
) -> None:
|
||||
"""Sanity check to make sure an Activity was destroyed properly.
|
||||
|
||||
Receives a weakref to a ba.Activity which should have torn itself
|
||||
|
|
@ -713,27 +735,20 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
and/or print debugging info if the Activity still exists.
|
||||
"""
|
||||
try:
|
||||
import gc
|
||||
import types
|
||||
activity = activity_ref()
|
||||
print('ERROR: Activity is not dying when expected:', activity,
|
||||
'(warning ' + str(counter[0] + 1) + ')')
|
||||
print('This means something is still strong-referencing it.')
|
||||
print(
|
||||
'ERROR: Activity is not dying when expected:',
|
||||
activity,
|
||||
'(warning ' + str(counter[0] + 1) + ')',
|
||||
)
|
||||
print(
|
||||
'This means something is still strong-referencing it.\n'
|
||||
'Check out methods such as efro.debug.printrefs() to'
|
||||
' help debug this sort of thing.'
|
||||
)
|
||||
# Note: no longer calling gc.get_referrers() here because it's
|
||||
# usage can bork stuff. (see notes at top of efro.debug)
|
||||
counter[0] += 1
|
||||
|
||||
# FIXME: Running the code below shows us references but winds up
|
||||
# keeping the object alive; need to figure out why.
|
||||
# For now we just print refs if the count gets to 3, and then we
|
||||
# kill the app at 4 so it doesn't matter anyway.
|
||||
if counter[0] == 3:
|
||||
print('Activity references for', activity, ':')
|
||||
refs = list(gc.get_referrers(activity))
|
||||
i = 1
|
||||
for ref in refs:
|
||||
if isinstance(ref, types.FrameType):
|
||||
continue
|
||||
print(' reference', i, ':', ref)
|
||||
i += 1
|
||||
if counter[0] == 4:
|
||||
print('Killing app due to stuck activity... :-(')
|
||||
_ba.quit()
|
||||
|
|
@ -784,8 +799,9 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
try:
|
||||
actor.on_expire()
|
||||
except Exception:
|
||||
print_exception(f'Error in Actor.on_expire()'
|
||||
f' for {actor_ref()}.')
|
||||
print_exception(
|
||||
f'Error in Actor.on_expire()' f' for {actor_ref()}.'
|
||||
)
|
||||
|
||||
def _expire_players(self) -> None:
|
||||
|
||||
|
|
|
|||
72
dist/ba_data/python/ba/_activitytypes.py
vendored
72
dist/ba_data/python/ba/_activitytypes.py
vendored
|
|
@ -9,6 +9,7 @@ import _ba
|
|||
from ba._activity import Activity
|
||||
from ba._music import setmusic, MusicType
|
||||
from ba._generated.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
|
||||
|
|
@ -40,6 +41,7 @@ class EndSessionActivity(Activity[EmptyPlayer, EmptyTeam]):
|
|||
# pylint: disable=cyclic-import
|
||||
from bastd.mainmenu import MainMenuSession
|
||||
from ba._general import Call
|
||||
|
||||
super().on_begin()
|
||||
_ba.unlock_all_input()
|
||||
_ba.app.ads.call_after_ad(Call(_ba.new_host_session, MainMenuSession))
|
||||
|
|
@ -72,10 +74,11 @@ class JoinActivity(Activity[EmptyPlayer, EmptyTeam]):
|
|||
# pylint: disable=cyclic-import
|
||||
from bastd.actor.tipstext import TipsText
|
||||
from bastd.actor.background import Background
|
||||
|
||||
super().on_transition_in()
|
||||
self._background = Background(fade_time=0.5,
|
||||
start_faded=True,
|
||||
show_logo=True)
|
||||
self._background = Background(
|
||||
fade_time=0.5, start_faded=True, show_logo=True
|
||||
)
|
||||
self._tips_text = TipsText()
|
||||
setmusic(MusicType.CHAR_SELECT)
|
||||
self._join_info = self.session.lobby.create_join_info()
|
||||
|
|
@ -103,10 +106,11 @@ class TransitionActivity(Activity[EmptyPlayer, EmptyTeam]):
|
|||
def on_transition_in(self) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.actor import background # FIXME: Don't use bastd from ba.
|
||||
|
||||
super().on_transition_in()
|
||||
self._background = background.Background(fade_time=0.5,
|
||||
start_faded=False,
|
||||
show_logo=False)
|
||||
self._background = background.Background(
|
||||
fade_time=0.5, start_faded=False, show_logo=False
|
||||
)
|
||||
|
||||
def on_begin(self) -> None:
|
||||
super().on_begin()
|
||||
|
|
@ -143,9 +147,11 @@ class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]):
|
|||
|
||||
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())
|
||||
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.
|
||||
|
|
@ -154,10 +160,11 @@ class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]):
|
|||
def on_transition_in(self) -> None:
|
||||
from bastd.actor.tipstext import TipsText
|
||||
from bastd.actor.background import Background
|
||||
|
||||
super().on_transition_in()
|
||||
self._background = Background(fade_time=0.5,
|
||||
start_faded=False,
|
||||
show_logo=True)
|
||||
self._background = Background(
|
||||
fade_time=0.5, start_faded=False, show_logo=True
|
||||
)
|
||||
if self._default_show_tips:
|
||||
self._tips_text = TipsText()
|
||||
setmusic(self.default_music)
|
||||
|
|
@ -166,6 +173,7 @@ class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]):
|
|||
# pylint: disable=cyclic-import
|
||||
from bastd.actor.text import Text
|
||||
from ba import _language
|
||||
|
||||
super().on_begin()
|
||||
import custom_hooks
|
||||
custom_hooks.score_screen_on_begin(self._stats)
|
||||
|
|
@ -180,24 +188,30 @@ class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]):
|
|||
else:
|
||||
sval = _language.Lstr(resource='pressAnyButtonText')
|
||||
|
||||
Text(self._custom_continue_message
|
||||
if self._custom_continue_message is not None else sval,
|
||||
v_attach=Text.VAttach.BOTTOM,
|
||||
h_align=Text.HAlign.CENTER,
|
||||
flash=True,
|
||||
vr_depth=50,
|
||||
position=(0, 10),
|
||||
scale=0.8,
|
||||
color=(0.5, 0.7, 0.5, 0.5),
|
||||
transition=Text.Transition.IN_BOTTOM_SLOW,
|
||||
transition_delay=self._min_view_time).autoretain()
|
||||
Text(
|
||||
self._custom_continue_message
|
||||
if self._custom_continue_message is not None
|
||||
else sval,
|
||||
v_attach=Text.VAttach.BOTTOM,
|
||||
h_align=Text.HAlign.CENTER,
|
||||
flash=True,
|
||||
vr_depth=50,
|
||||
position=(0, 10),
|
||||
scale=0.8,
|
||||
color=(0.5, 0.7, 0.5, 0.5),
|
||||
transition=Text.Transition.IN_BOTTOM_SLOW,
|
||||
transition_delay=self._min_view_time,
|
||||
).autoretain()
|
||||
|
||||
def _player_press(self) -> None:
|
||||
|
||||
# If this activity is a good 'end point', ask server-mode just once if
|
||||
# it wants to do anything special like switch sessions or kill the app.
|
||||
if (self._allow_server_transition and _ba.app.server is not None
|
||||
and self._server_transitioning is None):
|
||||
if (
|
||||
self._allow_server_transition
|
||||
and _ba.app.server is not None
|
||||
and self._server_transitioning is None
|
||||
):
|
||||
self._server_transitioning = _ba.app.server.handle_transition()
|
||||
assert isinstance(self._server_transitioning, bool)
|
||||
|
||||
|
|
@ -213,6 +227,12 @@ class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]):
|
|||
# Just to be extra careful, don't assign if we're transitioning out.
|
||||
# (though theoretically that should be ok).
|
||||
if not self.is_transitioning_out() and player:
|
||||
player.assigninput((InputType.JUMP_PRESS, InputType.PUNCH_PRESS,
|
||||
InputType.BOMB_PRESS, InputType.PICK_UP_PRESS),
|
||||
self._player_press)
|
||||
player.assigninput(
|
||||
(
|
||||
InputType.JUMP_PRESS,
|
||||
InputType.PUNCH_PRESS,
|
||||
InputType.BOMB_PRESS,
|
||||
InputType.PICK_UP_PRESS,
|
||||
),
|
||||
self._player_press,
|
||||
)
|
||||
|
|
|
|||
135
dist/ba_data/python/ba/_ads.py
vendored
135
dist/ba_data/python/ba/_ads.py
vendored
|
|
@ -38,31 +38,41 @@ class AdsSubsystem:
|
|||
|
||||
# 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)):
|
||||
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)
|
||||
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) -> None:
|
||||
def show_ad(
|
||||
self, purpose: str, on_completion_call: Callable[[], Any] | None = 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) -> None:
|
||||
self,
|
||||
purpose: str,
|
||||
on_completion_call: Callable[[bool], Any] | None = None,
|
||||
) -> None:
|
||||
"""(internal)"""
|
||||
self.last_ad_purpose = purpose
|
||||
_ba.show_ad_2(purpose, on_completion_call)
|
||||
|
|
@ -73,6 +83,7 @@ class AdsSubsystem:
|
|||
# pylint: disable=too-many-branches
|
||||
# pylint: disable=too-many-locals
|
||||
from ba._generated.enums import TimeType
|
||||
|
||||
app = _ba.app
|
||||
show = True
|
||||
|
||||
|
|
@ -95,16 +106,22 @@ class AdsSubsystem:
|
|||
launch_count = app.config.get('launchCount', 0)
|
||||
|
||||
# If we're seeing short ads we may want to space them differently.
|
||||
interval_mult = (_internal.get_v1_account_misc_read_val(
|
||||
'ads.shortIntervalMult', 1.0)
|
||||
if self.last_ad_was_short else 1.0)
|
||||
interval_mult = (
|
||||
_internal.get_v1_account_misc_read_val(
|
||||
'ads.shortIntervalMult', 1.0
|
||||
)
|
||||
if self.last_ad_was_short
|
||||
else 1.0
|
||||
)
|
||||
if self.ad_amt is None:
|
||||
if launch_count <= 1:
|
||||
self.ad_amt = _internal.get_v1_account_misc_read_val(
|
||||
'ads.startVal1', 0.99)
|
||||
'ads.startVal1', 0.99
|
||||
)
|
||||
else:
|
||||
self.ad_amt = _internal.get_v1_account_misc_read_val(
|
||||
'ads.startVal2', 1.0)
|
||||
'ads.startVal2', 1.0
|
||||
)
|
||||
interval = None
|
||||
else:
|
||||
# So far we're cleared to show; now calc our
|
||||
|
|
@ -113,27 +130,33 @@ class AdsSubsystem:
|
|||
# playing).
|
||||
base = 'ads' if _ba.has_video_ads() else 'ads2'
|
||||
min_lc = _internal.get_v1_account_misc_read_val(
|
||||
base + '.minLC', 0.0)
|
||||
base + '.minLC', 0.0
|
||||
)
|
||||
max_lc = _internal.get_v1_account_misc_read_val(
|
||||
base + '.maxLC', 5.0)
|
||||
min_lc_scale = (_internal.get_v1_account_misc_read_val(
|
||||
base + '.minLCScale', 0.25))
|
||||
max_lc_scale = (_internal.get_v1_account_misc_read_val(
|
||||
base + '.maxLCScale', 0.34))
|
||||
min_lc_interval = (_internal.get_v1_account_misc_read_val(
|
||||
base + '.minLCInterval', 360))
|
||||
max_lc_interval = (_internal.get_v1_account_misc_read_val(
|
||||
base + '.maxLCInterval', 300))
|
||||
base + '.maxLC', 5.0
|
||||
)
|
||||
min_lc_scale = _internal.get_v1_account_misc_read_val(
|
||||
base + '.minLCScale', 0.25
|
||||
)
|
||||
max_lc_scale = _internal.get_v1_account_misc_read_val(
|
||||
base + '.maxLCScale', 0.34
|
||||
)
|
||||
min_lc_interval = _internal.get_v1_account_misc_read_val(
|
||||
base + '.minLCInterval', 360
|
||||
)
|
||||
max_lc_interval = _internal.get_v1_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))
|
||||
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)
|
||||
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:
|
||||
|
|
@ -143,12 +166,14 @@ class AdsSubsystem:
|
|||
# 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)))):
|
||||
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:
|
||||
|
|
@ -161,7 +186,6 @@ class AdsSubsystem:
|
|||
# (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
|
||||
|
|
@ -171,20 +195,31 @@ class AdsSubsystem:
|
|||
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))
|
||||
(
|
||||
'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)
|
||||
_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.
|
||||
|
|
|
|||
30
dist/ba_data/python/ba/_analytics.py
vendored
30
dist/ba_data/python/ba/_analytics.py
vendored
|
|
@ -20,6 +20,7 @@ def game_begin_analytics() -> None:
|
|||
from ba._freeforallsession import FreeForAllSession
|
||||
from ba._coopsession import CoopSession
|
||||
from ba._gameactivity import GameActivity
|
||||
|
||||
activity = _ba.getactivity(False)
|
||||
session = _ba.getsession(False)
|
||||
|
||||
|
|
@ -31,8 +32,11 @@ def game_begin_analytics() -> None:
|
|||
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)
|
||||
'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')
|
||||
|
|
@ -49,9 +53,11 @@ def game_begin_analytics() -> None:
|
|||
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')
|
||||
_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')
|
||||
|
||||
|
|
@ -60,14 +66,18 @@ def game_begin_analytics() -> None:
|
|||
_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')
|
||||
'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')
|
||||
_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')
|
||||
'Free-for-all round start 8+ human players'
|
||||
)
|
||||
|
||||
# For some analytics tracking on the c layer.
|
||||
_ba.reset_game_activity_tracking()
|
||||
|
|
|
|||
99
dist/ba_data/python/ba/_app.py
vendored
99
dist/ba_data/python/ba/_app.py
vendored
|
|
@ -198,6 +198,7 @@ class App:
|
|||
accordingly and set to target the new API version number.
|
||||
"""
|
||||
from ba._meta import CURRENT_API_VERSION
|
||||
|
||||
return CURRENT_API_VERSION
|
||||
|
||||
@property
|
||||
|
|
@ -369,19 +370,33 @@ class App:
|
|||
|
||||
# FIXME: This should not be hard-coded.
|
||||
for maptype in [
|
||||
stdmaps.HockeyStadium, stdmaps.FootballStadium,
|
||||
stdmaps.Bridgit, stdmaps.BigG, stdmaps.Roundabout,
|
||||
stdmaps.MonkeyFace, stdmaps.ZigZag, stdmaps.ThePad,
|
||||
stdmaps.DoomShroom, stdmaps.LakeFrigid, stdmaps.TipTop,
|
||||
stdmaps.CragCastle, stdmaps.TowerD, stdmaps.HappyThoughts,
|
||||
stdmaps.StepRightUp, stdmaps.Courtyard, stdmaps.Rampage
|
||||
stdmaps.HockeyStadium,
|
||||
stdmaps.FootballStadium,
|
||||
stdmaps.Bridgit,
|
||||
stdmaps.BigG,
|
||||
stdmaps.Roundabout,
|
||||
stdmaps.MonkeyFace,
|
||||
stdmaps.ZigZag,
|
||||
stdmaps.ThePad,
|
||||
stdmaps.DoomShroom,
|
||||
stdmaps.LakeFrigid,
|
||||
stdmaps.TipTop,
|
||||
stdmaps.CragCastle,
|
||||
stdmaps.TowerD,
|
||||
stdmaps.HappyThoughts,
|
||||
stdmaps.StepRightUp,
|
||||
stdmaps.Courtyard,
|
||||
stdmaps.Rampage,
|
||||
]:
|
||||
_map.register_map(maptype)
|
||||
|
||||
# Non-test, non-debug builds should generally be blessed; warn if not.
|
||||
# (so I don't accidentally release a build that can't play tourneys)
|
||||
if (not self.debug_build and not self.test_build
|
||||
and not _internal.is_blessed()):
|
||||
if (
|
||||
not self.debug_build
|
||||
and not self.test_build
|
||||
and not _internal.is_blessed()
|
||||
):
|
||||
_ba.screenmessage('WARNING: NON-BLESSED BUILD', color=(1, 0, 0))
|
||||
|
||||
# If there's a leftover log file, attempt to upload it to the
|
||||
|
|
@ -393,6 +408,7 @@ class App:
|
|||
if not self.config_file_healthy:
|
||||
if self.platform in ('mac', 'linux', 'windows'):
|
||||
from bastd.ui import configerror
|
||||
|
||||
configerror.ConfigErrorWindow()
|
||||
return
|
||||
|
||||
|
|
@ -418,10 +434,13 @@ class App:
|
|||
# pending special offer.
|
||||
def check_special_offer() -> None:
|
||||
from bastd.ui.specialoffer import show_offer
|
||||
|
||||
config = self.config
|
||||
if ('pendingSpecialOffer' in config
|
||||
and _internal.get_public_login_id()
|
||||
== config['pendingSpecialOffer']['a']):
|
||||
if (
|
||||
'pendingSpecialOffer' in config
|
||||
and _internal.get_public_login_id()
|
||||
== config['pendingSpecialOffer']['a']
|
||||
):
|
||||
self.special_offer = config['pendingSpecialOffer']['o']
|
||||
show_offer()
|
||||
|
||||
|
|
@ -436,8 +455,9 @@ class App:
|
|||
|
||||
# See note below in on_app_pause.
|
||||
if self.state != self.State.LAUNCHING:
|
||||
logging.error('on_app_launch found state %s; expected LAUNCHING.',
|
||||
self.state)
|
||||
logging.error(
|
||||
'on_app_launch found state %s; expected LAUNCHING.', self.state
|
||||
)
|
||||
|
||||
self._launch_completed = True
|
||||
self._update_state()
|
||||
|
|
@ -501,6 +521,7 @@ class App:
|
|||
def read_config(self) -> None:
|
||||
"""(internal)"""
|
||||
from ba._appconfig import read_config
|
||||
|
||||
self._config, self.config_file_healthy = read_config()
|
||||
|
||||
def pause(self) -> None:
|
||||
|
|
@ -510,8 +531,11 @@ class App:
|
|||
to pause ..we now no longer pause if there are connected clients.
|
||||
"""
|
||||
activity: ba.Activity | None = _ba.get_foreground_host_activity()
|
||||
if (activity is not None and activity.allow_pausing
|
||||
and not _ba.have_connected_clients()):
|
||||
if (
|
||||
activity is not None
|
||||
and activity.allow_pausing
|
||||
and not _ba.have_connected_clients()
|
||||
):
|
||||
from ba._language import Lstr
|
||||
from ba._nodeactor import NodeActor
|
||||
|
||||
|
|
@ -525,13 +549,16 @@ class App:
|
|||
|
||||
# FIXME: This should not be an attr on Actor.
|
||||
activity.paused_text = NodeActor(
|
||||
_ba.newnode('text',
|
||||
attrs={
|
||||
'text': Lstr(resource='pausedByHostText'),
|
||||
'client_only': True,
|
||||
'flatness': 1.0,
|
||||
'h_align': 'center'
|
||||
}))
|
||||
_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.
|
||||
|
|
@ -562,13 +589,15 @@ class App:
|
|||
# Make note to add it to our challenges UI.
|
||||
self.custom_coop_practice_games.append(f'Challenges:{level.name}')
|
||||
|
||||
def return_to_main_menu_session_gracefully(self,
|
||||
reset_ui: bool = True) -> None:
|
||||
def return_to_main_menu_session_gracefully(
|
||||
self, reset_ui: bool = True
|
||||
) -> None:
|
||||
"""Attempt to cleanly get back to the main menu."""
|
||||
# pylint: disable=cyclic-import
|
||||
from ba import _benchmark
|
||||
from ba._general import Call
|
||||
from bastd.mainmenu import MainMenuSession
|
||||
|
||||
if reset_ui:
|
||||
_ba.app.ui.clear_main_menu_window()
|
||||
|
||||
|
|
@ -587,10 +616,9 @@ class App:
|
|||
|
||||
# Kick off a little transaction so we'll hopefully have all the
|
||||
# latest account state when we get back to the menu.
|
||||
_internal.add_transaction({
|
||||
'type': 'END_SESSION',
|
||||
'sType': str(type(host_session))
|
||||
})
|
||||
_internal.add_transaction(
|
||||
{'type': 'END_SESSION', 'sType': str(type(host_session))}
|
||||
)
|
||||
_internal.run_transactions()
|
||||
|
||||
host_session.end()
|
||||
|
|
@ -609,14 +637,14 @@ class App:
|
|||
else:
|
||||
self.main_menu_resume_callbacks.append(call)
|
||||
|
||||
def launch_coop_game(self,
|
||||
game: str,
|
||||
force: bool = False,
|
||||
args: dict | None = None) -> bool:
|
||||
def launch_coop_game(
|
||||
self, game: str, force: bool = False, args: dict | None = None
|
||||
) -> bool:
|
||||
"""High level way to launch a local co-op session."""
|
||||
# pylint: disable=cyclic-import
|
||||
from ba._campaign import getcampaign
|
||||
from bastd.ui.coop.level import CoopLevelLockedWindow
|
||||
|
||||
if args is None:
|
||||
args = {}
|
||||
if game == '':
|
||||
|
|
@ -633,7 +661,8 @@ class App:
|
|||
if not level.complete:
|
||||
CoopLevelLockedWindow(
|
||||
campaign.getlevel(levelname).displayname,
|
||||
campaign.getlevel(level.name).displayname)
|
||||
campaign.getlevel(level.name).displayname,
|
||||
)
|
||||
return False
|
||||
|
||||
# Ok, we're good to go.
|
||||
|
|
@ -646,12 +675,15 @@ class App:
|
|||
|
||||
def _fade_end() -> None:
|
||||
from ba import _coopsession
|
||||
|
||||
try:
|
||||
_ba.new_host_session(_coopsession.CoopSession)
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception()
|
||||
from bastd.mainmenu import MainMenuSession
|
||||
|
||||
_ba.new_host_session(MainMenuSession)
|
||||
|
||||
_ba.fade_screen(False, endcall=_fade_end)
|
||||
|
|
@ -660,6 +692,7 @@ class App:
|
|||
def handle_deep_link(self, url: str) -> None:
|
||||
"""Handle a deep link URL."""
|
||||
from ba._language import Lstr
|
||||
|
||||
appname = _ba.appname()
|
||||
if url.startswith(f'{appname}://code/'):
|
||||
code = url.replace(f'{appname}://code/', '')
|
||||
|
|
|
|||
30
dist/ba_data/python/ba/_appconfig.py
vendored
30
dist/ba_data/python/ba/_appconfig.py
vendored
|
|
@ -115,16 +115,29 @@ def read_config() -> tuple[AppConfig, bool]:
|
|||
config_file_healthy = True
|
||||
|
||||
except Exception as exc:
|
||||
print(('error reading config file at time ' +
|
||||
str(_ba.time(TimeType.REAL)) + ': \'' + config_file_path +
|
||||
'\':\n'), exc)
|
||||
print(
|
||||
(
|
||||
'error reading config file at time '
|
||||
+ str(_ba.time(TimeType.REAL))
|
||||
+ ': \''
|
||||
+ config_file_path
|
||||
+ '\':\n'
|
||||
),
|
||||
exc,
|
||||
)
|
||||
|
||||
# Whenever this happens lets back up the broken one just in case it
|
||||
# gets overwritten accidentally.
|
||||
print(('backing up current config file to \'' + config_file_path +
|
||||
".broken\'"))
|
||||
print(
|
||||
(
|
||||
'backing up current config file to \''
|
||||
+ config_file_path
|
||||
+ ".broken\'"
|
||||
)
|
||||
)
|
||||
try:
|
||||
import shutil
|
||||
|
||||
shutil.copyfile(config_file_path, config_file_path + '.broken')
|
||||
except Exception as exc2:
|
||||
print('EXC copying broken config:', exc2)
|
||||
|
|
@ -154,8 +167,11 @@ def commit_app_config(force: bool = False) -> None:
|
|||
(internal)
|
||||
"""
|
||||
from ba._internal import mark_config_dirty
|
||||
|
||||
if not _ba.app.config_file_healthy and not force:
|
||||
print('Current config file is broken; '
|
||||
'skipping write to avoid losing settings.')
|
||||
print(
|
||||
'Current config file is broken; '
|
||||
'skipping write to avoid losing settings.'
|
||||
)
|
||||
return
|
||||
mark_config_dirty()
|
||||
|
|
|
|||
13
dist/ba_data/python/ba/_appdelegate.py
vendored
13
dist/ba_data/python/ba/_appdelegate.py
vendored
|
|
@ -17,9 +17,12 @@ class AppDelegate:
|
|||
"""
|
||||
|
||||
def create_default_game_settings_ui(
|
||||
self, gameclass: type[ba.GameActivity],
|
||||
sessiontype: type[ba.Session], settings: dict | None,
|
||||
completion_call: Callable[[dict | None], None]) -> None:
|
||||
self,
|
||||
gameclass: type[ba.GameActivity],
|
||||
sessiontype: type[ba.Session],
|
||||
settings: dict | None,
|
||||
completion_call: Callable[[dict | None], None],
|
||||
) -> None:
|
||||
"""Launch a UI to configure the given game config.
|
||||
|
||||
It should manipulate the contents of config and call completion_call
|
||||
|
|
@ -27,5 +30,7 @@ class AppDelegate:
|
|||
"""
|
||||
del gameclass, sessiontype, settings, completion_call # Unused.
|
||||
from ba import _error
|
||||
|
||||
_error.print_error(
|
||||
"create_default_game_settings_ui needs to be overridden")
|
||||
"create_default_game_settings_ui needs to be overridden"
|
||||
)
|
||||
|
|
|
|||
63
dist/ba_data/python/ba/_apputils.py
vendored
63
dist/ba_data/python/ba/_apputils.py
vendored
|
|
@ -42,6 +42,7 @@ def is_browser_likely_available() -> bool:
|
|||
def get_remote_app_name() -> ba.Lstr:
|
||||
"""(internal)"""
|
||||
from ba import _language
|
||||
|
||||
return _language.Lstr(resource='remote_app.app_name')
|
||||
|
||||
|
||||
|
|
@ -59,6 +60,7 @@ def handle_v1_cloud_log() -> None:
|
|||
from ba._net import master_server_post
|
||||
from ba._generated.enums import TimeType
|
||||
from ba._internal import get_news_show
|
||||
|
||||
app = _ba.app
|
||||
app.log_have_new = True
|
||||
if not app.log_upload_timer_started:
|
||||
|
|
@ -112,10 +114,12 @@ def handle_v1_cloud_log() -> None:
|
|||
|
||||
if not _ba.is_log_full():
|
||||
with _ba.Context('ui'):
|
||||
_ba.timer(600.0,
|
||||
_reset,
|
||||
timetype=TimeType.REAL,
|
||||
suppress_format_warning=True)
|
||||
_ba.timer(
|
||||
600.0,
|
||||
_reset,
|
||||
timetype=TimeType.REAL,
|
||||
suppress_format_warning=True,
|
||||
)
|
||||
|
||||
|
||||
def handle_leftover_v1_cloud_log_file() -> None:
|
||||
|
|
@ -125,8 +129,9 @@ def handle_leftover_v1_cloud_log_file() -> None:
|
|||
from ba._net import master_server_post
|
||||
|
||||
if os.path.exists(_ba.get_v1_cloud_log_file_path()):
|
||||
with open(_ba.get_v1_cloud_log_file_path(),
|
||||
encoding='utf-8') as infile:
|
||||
with open(
|
||||
_ba.get_v1_cloud_log_file_path(), encoding='utf-8'
|
||||
) as infile:
|
||||
info = json.loads(infile.read())
|
||||
infile.close()
|
||||
do_send = should_submit_debug_info()
|
||||
|
|
@ -150,6 +155,7 @@ def handle_leftover_v1_cloud_log_file() -> None:
|
|||
os.remove(_ba.get_v1_cloud_log_file_path())
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception('Error handling leftover log file.')
|
||||
|
||||
|
||||
|
|
@ -164,7 +170,13 @@ def garbage_collect_session_end() -> None:
|
|||
print('PYTHON GC FOUND', len(gc.garbage), 'UNCOLLECTIBLE OBJECTS:')
|
||||
for i, obj in enumerate(gc.garbage):
|
||||
print(str(i) + ':', obj)
|
||||
print_live_object_warnings('after session shutdown')
|
||||
|
||||
# NOTE: no longer running these checks. Perhaps we can allow
|
||||
# running them with an explicit flag passed, but we should never
|
||||
# run them by default because gc.get_objects() can mess up the app.
|
||||
# See notes at top of efro.debug.
|
||||
if bool(False):
|
||||
print_live_object_warnings('after session shutdown')
|
||||
|
||||
|
||||
def garbage_collect() -> None:
|
||||
|
|
@ -180,10 +192,15 @@ def garbage_collect() -> None:
|
|||
|
||||
|
||||
def print_live_object_warnings(
|
||||
when: Any,
|
||||
ignore_session: ba.Session | None = None,
|
||||
ignore_activity: ba.Activity | None = None) -> None:
|
||||
"""Print warnings for remaining objects in the current context."""
|
||||
when: Any,
|
||||
ignore_session: ba.Session | None = None,
|
||||
ignore_activity: ba.Activity | None = None,
|
||||
) -> None:
|
||||
"""Print warnings for remaining objects in the current context.
|
||||
|
||||
IMPORTANT - don't call this in production; usage of gc.get_objects()
|
||||
can bork Python. See notes at top of efro.debug module.
|
||||
"""
|
||||
# pylint: disable=cyclic-import
|
||||
from ba._session import Session
|
||||
from ba._actor import Actor
|
||||
|
|
@ -229,13 +246,17 @@ def print_corrupt_file_error() -> None:
|
|||
"""Print an error if a corrupt file is found."""
|
||||
from ba._general import Call
|
||||
from ba._generated.enums import TimeType
|
||||
_ba.timer(2.0,
|
||||
lambda: _ba.screenmessage(
|
||||
_ba.app.lang.get_resource('internal.corruptFileText').
|
||||
replace('${EMAIL}', 'support@froemling.net'),
|
||||
color=(1, 0, 0),
|
||||
),
|
||||
timetype=TimeType.REAL)
|
||||
_ba.timer(2.0,
|
||||
Call(_ba.playsound, _ba.getsound('error')),
|
||||
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
|
||||
)
|
||||
|
|
|
|||
38
dist/ba_data/python/ba/_assetmanager.py
vendored
38
dist/ba_data/python/ba/_assetmanager.py
vendored
|
|
@ -15,8 +15,12 @@ import time
|
|||
import os
|
||||
import sys
|
||||
|
||||
from efro.dataclassio import (ioprepped, IOAttrs, dataclass_from_json,
|
||||
dataclass_to_json)
|
||||
from efro.dataclassio import (
|
||||
ioprepped,
|
||||
IOAttrs,
|
||||
dataclass_from_json,
|
||||
dataclass_to_json,
|
||||
)
|
||||
import _ba
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -34,8 +38,9 @@ class FileValue:
|
|||
class State:
|
||||
"""Holds all persistent state for the asset-manager."""
|
||||
|
||||
files: Annotated[dict[str, FileValue],
|
||||
IOAttrs('files')] = field(default_factory=dict)
|
||||
files: Annotated[dict[str, FileValue], IOAttrs('files')] = field(
|
||||
default_factory=dict
|
||||
)
|
||||
|
||||
|
||||
class AssetManager:
|
||||
|
|
@ -66,8 +71,14 @@ class AssetManager:
|
|||
account_token: str,
|
||||
) -> AssetGather:
|
||||
"""Spawn an asset-gather operation from this manager."""
|
||||
print('would gather', packages, 'and flavor', flavor, 'with token',
|
||||
account_token)
|
||||
print(
|
||||
'would gather',
|
||||
packages,
|
||||
'and flavor',
|
||||
flavor,
|
||||
'with token',
|
||||
account_token,
|
||||
)
|
||||
return AssetGather(self)
|
||||
|
||||
def update(self) -> None:
|
||||
|
|
@ -162,9 +173,7 @@ class AssetGather:
|
|||
|
||||
|
||||
def fetch_url(url: str, filename: Path, asset_gather: AssetGather) -> None:
|
||||
"""Fetch a given url to a given filename for a given AssetGather.
|
||||
|
||||
"""
|
||||
"""Fetch a given url to a given filename for a given AssetGather."""
|
||||
# pylint: disable=consider-using-with
|
||||
import socket
|
||||
|
||||
|
|
@ -175,9 +184,7 @@ def fetch_url(url: str, filename: Path, asset_gather: AssetGather) -> None:
|
|||
|
||||
# Pass a very short timeout to urllib so we have opportunities
|
||||
# to cancel even with network blockage.
|
||||
req = urllib.request.urlopen(url,
|
||||
context=_ba.app.net.sslcontext,
|
||||
timeout=1)
|
||||
req = urllib.request.urlopen(url, context=_ba.app.net.sslcontext, timeout=1)
|
||||
file_size = int(req.headers['Content-Length'])
|
||||
print(f'\nDownloading: {filename} Bytes: {file_size:,}')
|
||||
|
||||
|
|
@ -200,6 +207,7 @@ def fetch_url(url: str, filename: Path, asset_gather: AssetGather) -> None:
|
|||
data = req.read(block_sz)
|
||||
except ValueError:
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
print('VALUEERROR', flush=True)
|
||||
break
|
||||
|
|
@ -210,8 +218,10 @@ def fetch_url(url: str, filename: Path, asset_gather: AssetGather) -> None:
|
|||
print('\n\n\nsorry -- try back later')
|
||||
os.unlink(filename)
|
||||
raise
|
||||
print('\nHmmm... little issue... '
|
||||
'I\'ll wait a couple of seconds')
|
||||
print(
|
||||
'\nHmmm... little issue... '
|
||||
'I\'ll wait a couple of seconds'
|
||||
)
|
||||
time.sleep(3)
|
||||
time_outs += 1
|
||||
continue
|
||||
|
|
|
|||
11
dist/ba_data/python/ba/_asyncio.py
vendored
11
dist/ba_data/python/ba/_asyncio.py
vendored
|
|
@ -69,13 +69,14 @@ def setup_asyncio() -> asyncio.AbstractEventLoop:
|
|||
if duration > warn_time:
|
||||
logging.warning(
|
||||
'Asyncio loop step took %.4fs; ideal max is %.4f',
|
||||
duration, warn_time)
|
||||
duration,
|
||||
warn_time,
|
||||
)
|
||||
|
||||
global _asyncio_timer # pylint: disable=invalid-name
|
||||
_asyncio_timer = _ba.Timer(1.0 / 30.0,
|
||||
run_cycle,
|
||||
timetype=TimeType.REAL,
|
||||
repeat=True)
|
||||
_asyncio_timer = _ba.Timer(
|
||||
1.0 / 30.0, run_cycle, timetype=TimeType.REAL, repeat=True
|
||||
)
|
||||
|
||||
if bool(False):
|
||||
|
||||
|
|
|
|||
109
dist/ba_data/python/ba/_benchmark.py
vendored
109
dist/ba_data/python/ba/_benchmark.py
vendored
|
|
@ -50,31 +50,42 @@ def run_cpu_benchmark() -> None:
|
|||
_ba.new_host_session(BenchmarkSession, benchmark_type='cpu')
|
||||
|
||||
|
||||
def run_stress_test(playlist_type: str = 'Random',
|
||||
playlist_name: str = '__default__',
|
||||
player_count: int = 8,
|
||||
round_duration: int = 30) -> None:
|
||||
def run_stress_test(
|
||||
playlist_type: str = 'Random',
|
||||
playlist_name: str = '__default__',
|
||||
player_count: int = 8,
|
||||
round_duration: int = 30,
|
||||
) -> None:
|
||||
"""Run a stress test."""
|
||||
from ba import modutils
|
||||
from ba._general import Call
|
||||
from ba._generated.enums import TimeType
|
||||
|
||||
_ba.screenmessage(
|
||||
'Beginning stress test.. use '
|
||||
"'End Game' to stop testing.",
|
||||
color=(1, 1, 0))
|
||||
"Beginning stress test.. use 'End Test' to stop testing.",
|
||||
color=(1, 1, 0),
|
||||
)
|
||||
with _ba.Context('ui'):
|
||||
start_stress_test({
|
||||
'playlist_type': playlist_type,
|
||||
'playlist_name': playlist_name,
|
||||
'player_count': player_count,
|
||||
'round_duration': round_duration
|
||||
})
|
||||
_ba.timer(7.0,
|
||||
Call(_ba.screenmessage,
|
||||
('stats will be written to ' +
|
||||
modutils.get_human_readable_user_scripts_path() +
|
||||
'/stress_test_stats.csv')),
|
||||
timetype=TimeType.REAL)
|
||||
start_stress_test(
|
||||
{
|
||||
'playlist_type': playlist_type,
|
||||
'playlist_name': playlist_name,
|
||||
'player_count': player_count,
|
||||
'round_duration': round_duration,
|
||||
}
|
||||
)
|
||||
_ba.timer(
|
||||
7.0,
|
||||
Call(
|
||||
_ba.screenmessage,
|
||||
(
|
||||
'stats will be written to '
|
||||
+ modutils.get_human_readable_user_scripts_path()
|
||||
+ '/stress_test_stats.csv'
|
||||
),
|
||||
),
|
||||
timetype=TimeType.REAL,
|
||||
)
|
||||
|
||||
|
||||
def stop_stress_test() -> None:
|
||||
|
|
@ -94,6 +105,7 @@ def start_stress_test(args: dict[str, Any]) -> None:
|
|||
from ba._dualteamsession import DualTeamSession
|
||||
from ba._freeforallsession import FreeForAllSession
|
||||
from ba._generated.enums import TimeType, TimeFormat
|
||||
|
||||
appconfig = _ba.app.config
|
||||
playlist_type = args['playlist_type']
|
||||
if playlist_type == 'Random':
|
||||
|
|
@ -101,33 +113,42 @@ def start_stress_test(args: dict[str, Any]) -> None:
|
|||
playlist_type = 'Teams'
|
||||
else:
|
||||
playlist_type = 'Free-For-All'
|
||||
_ba.screenmessage('Running Stress Test (listType="' + playlist_type +
|
||||
'", listName="' + args['playlist_name'] + '")...')
|
||||
_ba.screenmessage(
|
||||
'Running Stress Test (listType="'
|
||||
+ playlist_type
|
||||
+ '", listName="'
|
||||
+ args['playlist_name']
|
||||
+ '")...'
|
||||
)
|
||||
if playlist_type == 'Teams':
|
||||
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)
|
||||
_ba.timer(
|
||||
1.0,
|
||||
Call(_ba.pushcall, Call(_ba.new_host_session, DualTeamSession)),
|
||||
timetype=TimeType.REAL,
|
||||
)
|
||||
else:
|
||||
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)),
|
||||
timetype=TimeType.REAL)
|
||||
_ba.timer(
|
||||
1.0,
|
||||
Call(_ba.pushcall, Call(_ba.new_host_session, FreeForAllSession)),
|
||||
timetype=TimeType.REAL,
|
||||
)
|
||||
_ba.set_stress_testing(True, args['player_count'])
|
||||
_ba.app.stress_test_reset_timer = _ba.Timer(
|
||||
args['round_duration'] * 1000,
|
||||
Call(_reset_stress_test, args),
|
||||
timetype=TimeType.REAL,
|
||||
timeformat=TimeFormat.MILLISECONDS)
|
||||
timeformat=TimeFormat.MILLISECONDS,
|
||||
)
|
||||
|
||||
|
||||
def _reset_stress_test(args: dict[str, Any]) -> None:
|
||||
from ba._general import Call
|
||||
from ba._generated.enums import TimeType
|
||||
|
||||
_ba.set_stress_testing(False, args['player_count'])
|
||||
_ba.screenmessage('Resetting stress test...')
|
||||
session = _ba.get_foreground_host_session()
|
||||
|
|
@ -138,34 +159,40 @@ def _reset_stress_test(args: dict[str, Any]) -> None:
|
|||
|
||||
def run_gpu_benchmark() -> None:
|
||||
"""Kick off a benchmark to test gpu speeds."""
|
||||
_ba.screenmessage('FIXME: Not wired up yet.', color=(1, 0, 0))
|
||||
# FIXME: Not wired up yet.
|
||||
_ba.screenmessage('Not wired up yet.', color=(1, 0, 0))
|
||||
|
||||
|
||||
def run_media_reload_benchmark() -> None:
|
||||
"""Kick off a benchmark to test media reloading speeds."""
|
||||
from ba._general import Call
|
||||
from ba._generated.enums import TimeType
|
||||
|
||||
_ba.reload_media()
|
||||
_ba.show_progress_bar()
|
||||
|
||||
def delay_add(start_time: float) -> None:
|
||||
|
||||
def doit(start_time_2: float) -> None:
|
||||
_ba.screenmessage(
|
||||
_ba.app.lang.get_resource(
|
||||
'debugWindow.totalReloadTimeText').replace(
|
||||
'${TIME}',
|
||||
str(_ba.time(TimeType.REAL) - start_time_2)))
|
||||
'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(_ba.app.lang.get_resource(
|
||||
'debugWindow.reloadBenchmarkBestResultsText'),
|
||||
color=(1, 1, 0))
|
||||
_ba.screenmessage(
|
||||
_ba.app.lang.get_resource(
|
||||
'debugWindow.reloadBenchmarkBestResultsText'
|
||||
),
|
||||
color=(1, 1, 0),
|
||||
)
|
||||
|
||||
_ba.add_clean_frame_callback(Call(doit, start_time))
|
||||
|
||||
# The reload starts (should add a completion callback to the
|
||||
# reload func to fix this).
|
||||
_ba.timer(0.05,
|
||||
Call(delay_add, _ba.time(TimeType.REAL)),
|
||||
timetype=TimeType.REAL)
|
||||
_ba.timer(
|
||||
0.05, Call(delay_add, _ba.time(TimeType.REAL)), timetype=TimeType.REAL
|
||||
)
|
||||
|
|
|
|||
50
dist/ba_data/python/ba/_bootstrap.py
vendored
50
dist/ba_data/python/ba/_bootstrap.py
vendored
|
|
@ -33,11 +33,13 @@ def bootstrap() -> None:
|
|||
# Python's stdout/stderr into it. Then we can at least debug problems
|
||||
# on systems where native stdout/stderr is not easily accessible
|
||||
# such as Android.
|
||||
log_handler = setup_logging(log_path=None,
|
||||
level=LogLevel.DEBUG,
|
||||
suppress_non_root_debug=True,
|
||||
log_stdout_stderr=True,
|
||||
cache_size_limit=1024 * 1024)
|
||||
log_handler = setup_logging(
|
||||
log_path=None,
|
||||
level=LogLevel.DEBUG,
|
||||
suppress_non_root_debug=True,
|
||||
log_stdout_stderr=True,
|
||||
cache_size_limit=1024 * 1024,
|
||||
)
|
||||
|
||||
log_handler.add_callback(_on_log)
|
||||
|
||||
|
|
@ -45,7 +47,7 @@ def bootstrap() -> None:
|
|||
|
||||
# Give a soft warning if we're being used with a different binary
|
||||
# version than we expect.
|
||||
expected_build = 20887
|
||||
expected_build = 20921
|
||||
running_build: int = env['build_number']
|
||||
if running_build != expected_build:
|
||||
print(
|
||||
|
|
@ -53,7 +55,8 @@ def bootstrap() -> None:
|
|||
f' Ballistica build {expected_build}.\n'
|
||||
f' You are running build {running_build}.'
|
||||
f' This might cause the app to error or misbehave.',
|
||||
file=sys.stderr)
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
# In bootstrap_monolithic.py we told Python not to handle SIGINT itself
|
||||
# (because that must be done in the main thread). Now we finish the
|
||||
|
|
@ -69,7 +72,8 @@ def bootstrap() -> None:
|
|||
print(
|
||||
'ERROR: Python\'s UTF-8 mode is not set.'
|
||||
' This will likely result in errors.',
|
||||
file=sys.stderr)
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
debug_build = env['debug_build']
|
||||
|
||||
|
|
@ -78,20 +82,24 @@ def bootstrap() -> None:
|
|||
print(
|
||||
f'WARNING: Mismatch in debug_build {debug_build}'
|
||||
f' and sys.flags.dev_mode {sys.flags.dev_mode}',
|
||||
file=sys.stderr)
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
# In embedded situations (when we're providing our own Python) let's
|
||||
# also provide our own root certs so ssl works. We can consider overriding
|
||||
# this in particular embedded cases if we can verify that system certs
|
||||
# are working.
|
||||
# (We also allow forcing this via an env var if the user desires)
|
||||
if (_ba.contains_python_dist()
|
||||
or os.environ.get('BA_USE_BUNDLED_ROOT_CERTS') == '1'):
|
||||
if (
|
||||
_ba.contains_python_dist()
|
||||
or os.environ.get('BA_USE_BUNDLED_ROOT_CERTS') == '1'
|
||||
):
|
||||
import certifi
|
||||
|
||||
# Let both OpenSSL and requests (if present) know to use this.
|
||||
os.environ['SSL_CERT_FILE'] = os.environ['REQUESTS_CA_BUNDLE'] = (
|
||||
certifi.where())
|
||||
os.environ['SSL_CERT_FILE'] = os.environ[
|
||||
'REQUESTS_CA_BUNDLE'
|
||||
] = certifi.where()
|
||||
|
||||
# On Windows I'm seeing the following error creating asyncio loops in
|
||||
# background threads with the default proactor setup:
|
||||
|
|
@ -104,6 +112,7 @@ def bootstrap() -> None:
|
|||
# to default to selector in that case?..
|
||||
if sys.platform == 'win32':
|
||||
import asyncio
|
||||
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||
|
||||
# pylint: disable=c-extension-no-member
|
||||
|
|
@ -122,6 +131,7 @@ def bootstrap() -> None:
|
|||
# Now spin up our App instance and store it on both _ba and ba.
|
||||
from ba._app import App
|
||||
import ba
|
||||
|
||||
_ba.app = ba.app = App()
|
||||
_ba.app.log_handler = log_handler
|
||||
|
||||
|
|
@ -138,6 +148,7 @@ class _CustomHelper:
|
|||
# (but then things mostly work). Let's get the ugly error out
|
||||
# of the way explicitly.
|
||||
import sysconfig
|
||||
|
||||
try:
|
||||
# This errors once but seems to run cleanly after, so let's
|
||||
# get the error out of the way.
|
||||
|
|
@ -146,13 +157,16 @@ class _CustomHelper:
|
|||
pass
|
||||
|
||||
import pydoc
|
||||
|
||||
# Disable pager and interactive help since neither works well
|
||||
# with our funky multi-threaded setup or in-game/cloud consoles.
|
||||
# Let's just do simple text dumps.
|
||||
pydoc.pager = pydoc.plainpager
|
||||
if not args and not kwds:
|
||||
print('Interactive help is not available in this environment.\n'
|
||||
'Type help(object) for help about object.')
|
||||
print(
|
||||
'Interactive help is not available in this environment.\n'
|
||||
'Type help(object) for help about object.'
|
||||
)
|
||||
return None
|
||||
return pydoc.help(*args, **kwds)
|
||||
|
||||
|
|
@ -170,6 +184,8 @@ def _on_log(entry: LogEntry) -> None:
|
|||
# We also want to feed some logs to the old V1-cloud-log system.
|
||||
# Let's go with anything warning or higher as well as the stdout/stderr
|
||||
# log messages that ba.app.log_handler creates for us.
|
||||
if entry.level.value >= LogLevel.WARNING.value or entry.name in ('stdout',
|
||||
'stderr'):
|
||||
if entry.level.value >= LogLevel.WARNING.value or entry.name in (
|
||||
'stdout',
|
||||
'stderr',
|
||||
):
|
||||
_ba.v1_cloud_log(entry.message)
|
||||
|
|
|
|||
475
dist/ba_data/python/ba/_campaign.py
vendored
475
dist/ba_data/python/ba/_campaign.py
vendored
|
|
@ -28,10 +28,12 @@ class Campaign:
|
|||
Category: **App Classes**
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
name: str,
|
||||
sequential: bool = True,
|
||||
levels: list[ba.Level] | None = None):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
sequential: bool = True,
|
||||
levels: list[ba.Level] | None = None,
|
||||
):
|
||||
self._name = name
|
||||
self._sequential = sequential
|
||||
self._levels: list[ba.Level] = []
|
||||
|
|
@ -67,12 +69,13 @@ class Campaign:
|
|||
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 _error.NotFoundError("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."""
|
||||
|
|
@ -91,9 +94,9 @@ class Campaign:
|
|||
@property
|
||||
def configdict(self) -> dict[str, Any]:
|
||||
"""Return the live config dict for this campaign."""
|
||||
val: dict[str, Any] = (_ba.app.config.setdefault('Campaigns',
|
||||
{}).setdefault(
|
||||
self._name, {}))
|
||||
val: dict[str, Any] = _ba.app.config.setdefault(
|
||||
'Campaigns', {}
|
||||
).setdefault(self._name, {})
|
||||
assert isinstance(val, dict)
|
||||
return val
|
||||
|
||||
|
|
@ -121,92 +124,132 @@ def init_campaigns() -> None:
|
|||
Campaign(
|
||||
'Easy',
|
||||
levels=[
|
||||
Level('Onslaught Training',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'training_easy'},
|
||||
preview_texture_name='doomShroomPreview'),
|
||||
Level('Rookie Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'rookie_easy'},
|
||||
preview_texture_name='courtyardPreview'),
|
||||
Level('Rookie Football',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'rookie_easy'},
|
||||
preview_texture_name='footballStadiumPreview'),
|
||||
Level('Pro Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'pro_easy'},
|
||||
preview_texture_name='doomShroomPreview'),
|
||||
Level('Pro Football',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'pro_easy'},
|
||||
preview_texture_name='footballStadiumPreview'),
|
||||
Level('Pro Runaround',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'pro_easy'},
|
||||
preview_texture_name='towerDPreview'),
|
||||
Level('Uber Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'uber_easy'},
|
||||
preview_texture_name='courtyardPreview'),
|
||||
Level('Uber Football',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'uber_easy'},
|
||||
preview_texture_name='footballStadiumPreview'),
|
||||
Level('Uber Runaround',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'uber_easy'},
|
||||
preview_texture_name='towerDPreview')
|
||||
Level(
|
||||
'Onslaught Training',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'training_easy'},
|
||||
preview_texture_name='doomShroomPreview',
|
||||
),
|
||||
Level(
|
||||
'Rookie Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'rookie_easy'},
|
||||
preview_texture_name='courtyardPreview',
|
||||
),
|
||||
Level(
|
||||
'Rookie Football',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'rookie_easy'},
|
||||
preview_texture_name='footballStadiumPreview',
|
||||
),
|
||||
Level(
|
||||
'Pro Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'pro_easy'},
|
||||
preview_texture_name='doomShroomPreview',
|
||||
),
|
||||
Level(
|
||||
'Pro Football',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'pro_easy'},
|
||||
preview_texture_name='footballStadiumPreview',
|
||||
),
|
||||
Level(
|
||||
'Pro Runaround',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'pro_easy'},
|
||||
preview_texture_name='towerDPreview',
|
||||
),
|
||||
Level(
|
||||
'Uber Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'uber_easy'},
|
||||
preview_texture_name='courtyardPreview',
|
||||
),
|
||||
Level(
|
||||
'Uber Football',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'uber_easy'},
|
||||
preview_texture_name='footballStadiumPreview',
|
||||
),
|
||||
Level(
|
||||
'Uber Runaround',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'uber_easy'},
|
||||
preview_texture_name='towerDPreview',
|
||||
),
|
||||
],
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
# "hard" mode
|
||||
register_campaign(
|
||||
Campaign(
|
||||
'Default',
|
||||
levels=[
|
||||
Level('Onslaught Training',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'training'},
|
||||
preview_texture_name='doomShroomPreview'),
|
||||
Level('Rookie Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'rookie'},
|
||||
preview_texture_name='courtyardPreview'),
|
||||
Level('Rookie Football',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'rookie'},
|
||||
preview_texture_name='footballStadiumPreview'),
|
||||
Level('Pro Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'pro'},
|
||||
preview_texture_name='doomShroomPreview'),
|
||||
Level('Pro Football',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'pro'},
|
||||
preview_texture_name='footballStadiumPreview'),
|
||||
Level('Pro Runaround',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'pro'},
|
||||
preview_texture_name='towerDPreview'),
|
||||
Level('Uber Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'uber'},
|
||||
preview_texture_name='courtyardPreview'),
|
||||
Level('Uber Football',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'uber'},
|
||||
preview_texture_name='footballStadiumPreview'),
|
||||
Level('Uber Runaround',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'uber'},
|
||||
preview_texture_name='towerDPreview'),
|
||||
Level('The Last Stand',
|
||||
gametype=TheLastStandGame,
|
||||
settings={},
|
||||
preview_texture_name='rampagePreview')
|
||||
Level(
|
||||
'Onslaught Training',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'training'},
|
||||
preview_texture_name='doomShroomPreview',
|
||||
),
|
||||
Level(
|
||||
'Rookie Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'rookie'},
|
||||
preview_texture_name='courtyardPreview',
|
||||
),
|
||||
Level(
|
||||
'Rookie Football',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'rookie'},
|
||||
preview_texture_name='footballStadiumPreview',
|
||||
),
|
||||
Level(
|
||||
'Pro Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'pro'},
|
||||
preview_texture_name='doomShroomPreview',
|
||||
),
|
||||
Level(
|
||||
'Pro Football',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'pro'},
|
||||
preview_texture_name='footballStadiumPreview',
|
||||
),
|
||||
Level(
|
||||
'Pro Runaround',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'pro'},
|
||||
preview_texture_name='towerDPreview',
|
||||
),
|
||||
Level(
|
||||
'Uber Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'uber'},
|
||||
preview_texture_name='courtyardPreview',
|
||||
),
|
||||
Level(
|
||||
'Uber Football',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'uber'},
|
||||
preview_texture_name='footballStadiumPreview',
|
||||
),
|
||||
Level(
|
||||
'Uber Runaround',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'uber'},
|
||||
preview_texture_name='towerDPreview',
|
||||
),
|
||||
Level(
|
||||
'The Last Stand',
|
||||
gametype=TheLastStandGame,
|
||||
settings={},
|
||||
preview_texture_name='rampagePreview',
|
||||
),
|
||||
],
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
# challenges: our 'official' random extra co-op levels
|
||||
register_campaign(
|
||||
|
|
@ -214,121 +257,153 @@ def init_campaigns() -> None:
|
|||
'Challenges',
|
||||
sequential=False,
|
||||
levels=[
|
||||
Level('Infinite Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'endless'},
|
||||
preview_texture_name='doomShroomPreview'),
|
||||
Level('Infinite Runaround',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'endless'},
|
||||
preview_texture_name='towerDPreview'),
|
||||
Level('Race',
|
||||
displayname='${GAME}',
|
||||
gametype=RaceGame,
|
||||
settings={
|
||||
'map': 'Big G',
|
||||
'Laps': 3,
|
||||
'Bomb Spawning': 0
|
||||
},
|
||||
preview_texture_name='bigGPreview'),
|
||||
Level('Pro Race',
|
||||
displayname='Pro ${GAME}',
|
||||
gametype=RaceGame,
|
||||
settings={
|
||||
'map': 'Big G',
|
||||
'Laps': 3,
|
||||
'Bomb Spawning': 1000
|
||||
},
|
||||
preview_texture_name='bigGPreview'),
|
||||
Level('Lake Frigid Race',
|
||||
displayname='${GAME}',
|
||||
gametype=RaceGame,
|
||||
settings={
|
||||
'map': 'Lake Frigid',
|
||||
'Laps': 6,
|
||||
'Mine Spawning': 2000,
|
||||
'Bomb Spawning': 0
|
||||
},
|
||||
preview_texture_name='lakeFrigidPreview'),
|
||||
Level('Football',
|
||||
displayname='${GAME}',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'tournament'},
|
||||
preview_texture_name='footballStadiumPreview'),
|
||||
Level('Pro Football',
|
||||
displayname='Pro ${GAME}',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'tournament_pro'},
|
||||
preview_texture_name='footballStadiumPreview'),
|
||||
Level('Runaround',
|
||||
displayname='${GAME}',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'tournament'},
|
||||
preview_texture_name='towerDPreview'),
|
||||
Level('Uber Runaround',
|
||||
displayname='Uber ${GAME}',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'tournament_uber'},
|
||||
preview_texture_name='towerDPreview'),
|
||||
Level('The Last Stand',
|
||||
displayname='${GAME}',
|
||||
gametype=TheLastStandGame,
|
||||
settings={'preset': 'tournament'},
|
||||
preview_texture_name='rampagePreview'),
|
||||
Level('Tournament Infinite Onslaught',
|
||||
displayname='Infinite Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'endless_tournament'},
|
||||
preview_texture_name='doomShroomPreview'),
|
||||
Level('Tournament Infinite Runaround',
|
||||
displayname='Infinite Runaround',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'endless_tournament'},
|
||||
preview_texture_name='towerDPreview'),
|
||||
Level('Target Practice',
|
||||
displayname='Pro ${GAME}',
|
||||
gametype=TargetPracticeGame,
|
||||
settings={},
|
||||
preview_texture_name='doomShroomPreview'),
|
||||
Level('Target Practice B',
|
||||
displayname='${GAME}',
|
||||
gametype=TargetPracticeGame,
|
||||
settings={
|
||||
'Target Count': 2,
|
||||
'Enable Impact Bombs': False,
|
||||
'Enable Triple Bombs': False
|
||||
},
|
||||
preview_texture_name='doomShroomPreview'),
|
||||
Level('Meteor Shower',
|
||||
displayname='${GAME}',
|
||||
gametype=MeteorShowerGame,
|
||||
settings={},
|
||||
preview_texture_name='rampagePreview'),
|
||||
Level('Epic Meteor Shower',
|
||||
displayname='${GAME}',
|
||||
gametype=MeteorShowerGame,
|
||||
settings={'Epic Mode': True},
|
||||
preview_texture_name='rampagePreview'),
|
||||
Level('Easter Egg Hunt',
|
||||
displayname='${GAME}',
|
||||
gametype=EasterEggHuntGame,
|
||||
settings={},
|
||||
preview_texture_name='towerDPreview'),
|
||||
Level('Pro Easter Egg Hunt',
|
||||
displayname='Pro ${GAME}',
|
||||
gametype=EasterEggHuntGame,
|
||||
settings={'Pro Mode': True},
|
||||
preview_texture_name='towerDPreview'),
|
||||
Level(
|
||||
'Infinite Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'endless'},
|
||||
preview_texture_name='doomShroomPreview',
|
||||
),
|
||||
Level(
|
||||
'Infinite Runaround',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'endless'},
|
||||
preview_texture_name='towerDPreview',
|
||||
),
|
||||
Level(
|
||||
'Race',
|
||||
displayname='${GAME}',
|
||||
gametype=RaceGame,
|
||||
settings={'map': 'Big G', 'Laps': 3, 'Bomb Spawning': 0},
|
||||
preview_texture_name='bigGPreview',
|
||||
),
|
||||
Level(
|
||||
'Pro Race',
|
||||
displayname='Pro ${GAME}',
|
||||
gametype=RaceGame,
|
||||
settings={'map': 'Big G', 'Laps': 3, 'Bomb Spawning': 1000},
|
||||
preview_texture_name='bigGPreview',
|
||||
),
|
||||
Level(
|
||||
'Lake Frigid Race',
|
||||
displayname='${GAME}',
|
||||
gametype=RaceGame,
|
||||
settings={
|
||||
'map': 'Lake Frigid',
|
||||
'Laps': 6,
|
||||
'Mine Spawning': 2000,
|
||||
'Bomb Spawning': 0,
|
||||
},
|
||||
preview_texture_name='lakeFrigidPreview',
|
||||
),
|
||||
Level(
|
||||
'Football',
|
||||
displayname='${GAME}',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'tournament'},
|
||||
preview_texture_name='footballStadiumPreview',
|
||||
),
|
||||
Level(
|
||||
'Pro Football',
|
||||
displayname='Pro ${GAME}',
|
||||
gametype=FootballCoopGame,
|
||||
settings={'preset': 'tournament_pro'},
|
||||
preview_texture_name='footballStadiumPreview',
|
||||
),
|
||||
Level(
|
||||
'Runaround',
|
||||
displayname='${GAME}',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'tournament'},
|
||||
preview_texture_name='towerDPreview',
|
||||
),
|
||||
Level(
|
||||
'Uber Runaround',
|
||||
displayname='Uber ${GAME}',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'tournament_uber'},
|
||||
preview_texture_name='towerDPreview',
|
||||
),
|
||||
Level(
|
||||
'The Last Stand',
|
||||
displayname='${GAME}',
|
||||
gametype=TheLastStandGame,
|
||||
settings={'preset': 'tournament'},
|
||||
preview_texture_name='rampagePreview',
|
||||
),
|
||||
Level(
|
||||
'Tournament Infinite Onslaught',
|
||||
displayname='Infinite Onslaught',
|
||||
gametype=OnslaughtGame,
|
||||
settings={'preset': 'endless_tournament'},
|
||||
preview_texture_name='doomShroomPreview',
|
||||
),
|
||||
Level(
|
||||
'Tournament Infinite Runaround',
|
||||
displayname='Infinite Runaround',
|
||||
gametype=RunaroundGame,
|
||||
settings={'preset': 'endless_tournament'},
|
||||
preview_texture_name='towerDPreview',
|
||||
),
|
||||
Level(
|
||||
'Target Practice',
|
||||
displayname='Pro ${GAME}',
|
||||
gametype=TargetPracticeGame,
|
||||
settings={},
|
||||
preview_texture_name='doomShroomPreview',
|
||||
),
|
||||
Level(
|
||||
'Target Practice B',
|
||||
displayname='${GAME}',
|
||||
gametype=TargetPracticeGame,
|
||||
settings={
|
||||
'Target Count': 2,
|
||||
'Enable Impact Bombs': False,
|
||||
'Enable Triple Bombs': False,
|
||||
},
|
||||
preview_texture_name='doomShroomPreview',
|
||||
),
|
||||
Level(
|
||||
'Meteor Shower',
|
||||
displayname='${GAME}',
|
||||
gametype=MeteorShowerGame,
|
||||
settings={},
|
||||
preview_texture_name='rampagePreview',
|
||||
),
|
||||
Level(
|
||||
'Epic Meteor Shower',
|
||||
displayname='${GAME}',
|
||||
gametype=MeteorShowerGame,
|
||||
settings={'Epic Mode': True},
|
||||
preview_texture_name='rampagePreview',
|
||||
),
|
||||
Level(
|
||||
'Easter Egg Hunt',
|
||||
displayname='${GAME}',
|
||||
gametype=EasterEggHuntGame,
|
||||
settings={},
|
||||
preview_texture_name='towerDPreview',
|
||||
),
|
||||
Level(
|
||||
'Pro Easter Egg Hunt',
|
||||
displayname='Pro ${GAME}',
|
||||
gametype=EasterEggHuntGame,
|
||||
settings={'Pro Mode': True},
|
||||
preview_texture_name='towerDPreview',
|
||||
),
|
||||
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'),
|
||||
Level(name='Pro Ninja Fight',
|
||||
displayname='Pro ${GAME}',
|
||||
gametype=NinjaFightGame,
|
||||
settings={'preset': 'pro'},
|
||||
preview_texture_name='courtyardPreview')
|
||||
preview_texture_name='courtyardPreview',
|
||||
),
|
||||
Level(
|
||||
name='Pro Ninja Fight',
|
||||
displayname='Pro ${GAME}',
|
||||
gametype=NinjaFightGame,
|
||||
settings={'preset': 'pro'},
|
||||
preview_texture_name='courtyardPreview',
|
||||
),
|
||||
],
|
||||
))
|
||||
)
|
||||
)
|
||||
|
|
|
|||
23
dist/ba_data/python/ba/_cloud.py
vendored
23
dist/ba_data/python/ba/_cloud.py
vendored
|
|
@ -35,7 +35,8 @@ class CloudSubsystem:
|
|||
self,
|
||||
msg: bacommon.cloud.LoginProxyRequestMessage,
|
||||
on_response: Callable[
|
||||
[bacommon.cloud.LoginProxyRequestResponse | Exception], None],
|
||||
[bacommon.cloud.LoginProxyRequestResponse | Exception], None
|
||||
],
|
||||
) -> None:
|
||||
...
|
||||
|
||||
|
|
@ -44,7 +45,8 @@ class CloudSubsystem:
|
|||
self,
|
||||
msg: bacommon.cloud.LoginProxyStateQueryMessage,
|
||||
on_response: Callable[
|
||||
[bacommon.cloud.LoginProxyStateQueryResponse | Exception], None],
|
||||
[bacommon.cloud.LoginProxyStateQueryResponse | Exception], None
|
||||
],
|
||||
) -> None:
|
||||
...
|
||||
|
||||
|
|
@ -75,11 +77,15 @@ class CloudSubsystem:
|
|||
and passed either the response or the error that occurred.
|
||||
"""
|
||||
from ba._general import Call
|
||||
|
||||
del msg # Unused.
|
||||
|
||||
_ba.pushcall(
|
||||
Call(on_response,
|
||||
RuntimeError('Cloud functionality is not available.')))
|
||||
Call(
|
||||
on_response,
|
||||
RuntimeError('Cloud functionality is not available.'),
|
||||
)
|
||||
)
|
||||
|
||||
@overload
|
||||
def send_message(
|
||||
|
|
@ -89,8 +95,8 @@ class CloudSubsystem:
|
|||
|
||||
@overload
|
||||
def send_message(
|
||||
self,
|
||||
msg: bacommon.cloud.TestMessage) -> bacommon.cloud.TestResponse:
|
||||
self, msg: bacommon.cloud.TestMessage
|
||||
) -> bacommon.cloud.TestResponse:
|
||||
...
|
||||
|
||||
def send_message(self, msg: Message) -> Response | None:
|
||||
|
|
@ -107,6 +113,7 @@ def cloud_console_exec(code: str) -> None:
|
|||
import logging
|
||||
import __main__
|
||||
from ba._generated.enums import TimeType
|
||||
|
||||
try:
|
||||
|
||||
# First try it as eval.
|
||||
|
|
@ -118,7 +125,8 @@ def cloud_console_exec(code: str) -> None:
|
|||
# hmm; when we can't compile it as eval will we always get
|
||||
# syntax error?
|
||||
logging.exception(
|
||||
'unexpected error compiling code for cloud-console eval.')
|
||||
'unexpected error compiling code for cloud-console eval.'
|
||||
)
|
||||
evalcode = None
|
||||
if evalcode is not None:
|
||||
# pylint: disable=eval-used
|
||||
|
|
@ -135,6 +143,7 @@ def cloud_console_exec(code: str) -> None:
|
|||
exec(execcode, vars(__main__), vars(__main__))
|
||||
except Exception:
|
||||
import traceback
|
||||
|
||||
apptime = _ba.time(TimeType.REAL)
|
||||
print(f'Exec error at time {apptime:.2f}.', file=sys.stderr)
|
||||
traceback.print_exc()
|
||||
|
|
|
|||
162
dist/ba_data/python/ba/_coopgame.py
vendored
162
dist/ba_data/python/ba/_coopgame.py
vendored
|
|
@ -33,6 +33,7 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
|
|||
@classmethod
|
||||
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
|
||||
from ba._coopsession import CoopSession
|
||||
|
||||
return issubclass(sessiontype, CoopSession)
|
||||
|
||||
def __init__(self, settings: dict):
|
||||
|
|
@ -55,12 +56,14 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
|
|||
# Preload achievement images in case we get some.
|
||||
_ba.timer(2.0, WeakCall(self._preload_achievements))
|
||||
|
||||
def _show_standard_scores_to_beat_ui(self,
|
||||
scores: list[dict[str, Any]]) -> None:
|
||||
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._generated.enums import TimeFormat
|
||||
|
||||
display_type = self.get_score_type()
|
||||
if scores is not None:
|
||||
|
||||
|
|
@ -70,26 +73,35 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
|
|||
# Now make a display for the most recent challenge.
|
||||
for score in scores:
|
||||
if score['type'] == 'score_challenge':
|
||||
tval = (score['player'] + ': ' + timestring(
|
||||
int(score['value']) * 10,
|
||||
timeformat=TimeFormat.MILLISECONDS).evaluate()
|
||||
if display_type == 'time' else str(score['value']))
|
||||
tval = (
|
||||
score['player']
|
||||
+ ': '
|
||||
+ timestring(
|
||||
int(score['value']) * 10,
|
||||
timeformat=TimeFormat.MILLISECONDS,
|
||||
).evaluate()
|
||||
if display_type == 'time'
|
||||
else str(score['value'])
|
||||
)
|
||||
hattach = 'center' if display_type == 'time' else 'left'
|
||||
halign = 'center' if display_type == 'time' else 'left'
|
||||
pos = (20, -70) if display_type == 'time' else (20, -130)
|
||||
txt = NodeActor(
|
||||
_ba.newnode('text',
|
||||
attrs={
|
||||
'v_attach': 'top',
|
||||
'h_attach': hattach,
|
||||
'h_align': halign,
|
||||
'color': (0.7, 0.4, 1, 1),
|
||||
'shadow': 0.5,
|
||||
'flatness': 1.0,
|
||||
'position': pos,
|
||||
'scale': 0.6,
|
||||
'text': tval
|
||||
})).autoretain()
|
||||
_ba.newnode(
|
||||
'text',
|
||||
attrs={
|
||||
'v_attach': 'top',
|
||||
'h_attach': hattach,
|
||||
'h_align': halign,
|
||||
'color': (0.7, 0.4, 1, 1),
|
||||
'shadow': 0.5,
|
||||
'flatness': 1.0,
|
||||
'position': pos,
|
||||
'scale': 0.6,
|
||||
'text': tval,
|
||||
},
|
||||
)
|
||||
).autoretain()
|
||||
assert txt.node is not None
|
||||
animate(txt.node, 'scale', {1.0: 0.0, 1.1: 0.7, 1.2: 0.6})
|
||||
break
|
||||
|
|
@ -104,8 +116,7 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
|
|||
|
||||
def _get_coop_level_name(self) -> str:
|
||||
assert self.session.campaign is not None
|
||||
return self.session.campaign.name + ':' + str(
|
||||
self.settings_raw['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.
|
||||
|
|
@ -115,13 +126,15 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
|
|||
duration is given in seconds.
|
||||
"""
|
||||
from ba._messages import CelebrateMessage
|
||||
|
||||
for player in self.players:
|
||||
if player.actor:
|
||||
player.actor.handlemessage(CelebrateMessage(duration))
|
||||
|
||||
def _preload_achievements(self) -> None:
|
||||
achievements = _ba.app.ach.achievements_for_coop_level(
|
||||
self._get_coop_level_name())
|
||||
self._get_coop_level_name()
|
||||
)
|
||||
for ach in achievements:
|
||||
ach.get_icon_texture(True)
|
||||
|
||||
|
|
@ -129,43 +142,52 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
|
|||
# pylint: disable=cyclic-import
|
||||
from ba._language import Lstr
|
||||
from bastd.actor.text import Text
|
||||
|
||||
ts_h_offs = 30
|
||||
v_offs = -200
|
||||
achievements = [
|
||||
a for a in _ba.app.ach.achievements_for_coop_level(
|
||||
self._get_coop_level_name()) if not a.complete
|
||||
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(Lstr(resource='achievementsRemainingText'),
|
||||
host_only=True,
|
||||
position=(ts_h_offs - 10 + 40, v_offs - 10),
|
||||
transition=Text.Transition.FADE_IN,
|
||||
scale=1.1,
|
||||
h_attach=Text.HAttach.LEFT,
|
||||
v_attach=Text.VAttach.TOP,
|
||||
color=(1, 1, 1.2, 1) if vrmode else (0.8, 0.8, 1.0, 1.0),
|
||||
flatness=1.0 if vrmode else 0.6,
|
||||
shadow=1.0 if vrmode else 0.5,
|
||||
transition_delay=0.0,
|
||||
transition_out_delay=1.3
|
||||
if self.slow_motion else 4.0).autoretain()
|
||||
Text(
|
||||
Lstr(resource='achievementsRemainingText'),
|
||||
host_only=True,
|
||||
position=(ts_h_offs - 10 + 40, v_offs - 10),
|
||||
transition=Text.Transition.FADE_IN,
|
||||
scale=1.1,
|
||||
h_attach=Text.HAttach.LEFT,
|
||||
v_attach=Text.VAttach.TOP,
|
||||
color=(1, 1, 1.2, 1) if vrmode else (0.8, 0.8, 1.0, 1.0),
|
||||
flatness=1.0 if vrmode else 0.6,
|
||||
shadow=1.0 if vrmode else 0.5,
|
||||
transition_delay=0.0,
|
||||
transition_out_delay=1.3 if self.slow_motion else 4.0,
|
||||
).autoretain()
|
||||
hval = 70
|
||||
vval = -50
|
||||
tdelay = 0.0
|
||||
for ach in achievements:
|
||||
tdelay += 0.05
|
||||
ach.create_display(hval + 40,
|
||||
vval + v_offs,
|
||||
0 + tdelay,
|
||||
outdelay=1.3 if self.slow_motion else 4.0,
|
||||
style='in_game')
|
||||
ach.create_display(
|
||||
hval + 40,
|
||||
vval + v_offs,
|
||||
0 + tdelay,
|
||||
outdelay=1.3 if self.slow_motion else 4.0,
|
||||
style='in_game',
|
||||
)
|
||||
vval -= 55
|
||||
|
||||
def spawn_player_spaz(self,
|
||||
player: PlayerType,
|
||||
position: Sequence[float] = (0.0, 0.0, 0.0),
|
||||
angle: float | None = None) -> PlayerSpaz:
|
||||
def spawn_player_spaz(
|
||||
self,
|
||||
player: PlayerType,
|
||||
position: Sequence[float] = (0.0, 0.0, 0.0),
|
||||
angle: float | None = None,
|
||||
) -> PlayerSpaz:
|
||||
"""Spawn and wire up a standard player spaz."""
|
||||
spaz = super().spawn_player_spaz(player, position, angle)
|
||||
|
||||
|
|
@ -173,9 +195,9 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
|
|||
spaz.play_big_death_sound = True
|
||||
return spaz
|
||||
|
||||
def _award_achievement(self,
|
||||
achievement_name: str,
|
||||
sound: bool = True) -> None:
|
||||
def _award_achievement(
|
||||
self, achievement_name: str, sound: bool = True
|
||||
) -> None:
|
||||
"""Award an achievement.
|
||||
|
||||
Returns True if a banner will be shown;
|
||||
|
|
@ -196,6 +218,7 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
|
|||
return
|
||||
except Exception:
|
||||
from ba._error import print_exception
|
||||
|
||||
print_exception()
|
||||
|
||||
# If we haven't awarded this one, check to see if we've got it.
|
||||
|
|
@ -208,10 +231,9 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
|
|||
_internal.report_achievement(achievement_name)
|
||||
|
||||
# ...and to our account.
|
||||
_internal.add_transaction({
|
||||
'type': 'ACHIEVEMENT',
|
||||
'name': achievement_name
|
||||
})
|
||||
_internal.add_transaction(
|
||||
{'type': 'ACHIEVEMENT', 'name': achievement_name}
|
||||
)
|
||||
|
||||
# Now bring up a celebration banner.
|
||||
ach.announce_completion(sound=sound)
|
||||
|
|
@ -219,14 +241,17 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
|
|||
def fade_to_red(self) -> None:
|
||||
"""Fade the screen to red; (such as when the good guys have lost)."""
|
||||
from ba import _gameutils
|
||||
|
||||
c_existing = self.globalsnode.tint
|
||||
cnode = _ba.newnode('combine',
|
||||
attrs={
|
||||
'input0': c_existing[0],
|
||||
'input1': c_existing[1],
|
||||
'input2': c_existing[2],
|
||||
'size': 3
|
||||
})
|
||||
cnode = _ba.newnode(
|
||||
'combine',
|
||||
attrs={
|
||||
'input0': c_existing[0],
|
||||
'input1': c_existing[1],
|
||||
'input2': c_existing[2],
|
||||
'size': 3,
|
||||
},
|
||||
)
|
||||
_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', self.globalsnode, 'tint')
|
||||
|
|
@ -235,7 +260,8 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
|
|||
"""Set up a beeping noise to play when any players are near death."""
|
||||
self._life_warning_beep = None
|
||||
self._life_warning_beep_timer = _ba.Timer(
|
||||
1.0, 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.
|
||||
|
|
@ -249,12 +275,16 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
|
|||
break
|
||||
if should_beep and self._life_warning_beep is None:
|
||||
from ba._nodeactor import NodeActor
|
||||
|
||||
self._life_warning_beep = NodeActor(
|
||||
_ba.newnode('sound',
|
||||
attrs={
|
||||
'sound': self._warn_beeps_sound,
|
||||
'positional': False,
|
||||
'loop': True
|
||||
}))
|
||||
_ba.newnode(
|
||||
'sound',
|
||||
attrs={
|
||||
'sound': self._warn_beeps_sound,
|
||||
'positional': False,
|
||||
'loop': True,
|
||||
},
|
||||
)
|
||||
)
|
||||
if self._life_warning_beep is not None and not should_beep:
|
||||
self._life_warning_beep = None
|
||||
|
|
|
|||
117
dist/ba_data/python/ba/_coopsession.py
vendored
117
dist/ba_data/python/ba/_coopsession.py
vendored
|
|
@ -65,15 +65,18 @@ class CoopSession(Session):
|
|||
# print('FIXME: COOP SESSION WOULD CALC DEPS.')
|
||||
depsets: Sequence[ba.DependencySet] = []
|
||||
|
||||
super().__init__(depsets,
|
||||
team_names=TEAM_NAMES,
|
||||
team_colors=TEAM_COLORS,
|
||||
min_players=min_players,
|
||||
max_players=max_players)
|
||||
super().__init__(
|
||||
depsets,
|
||||
team_names=TEAM_NAMES,
|
||||
team_colors=TEAM_COLORS,
|
||||
min_players=min_players,
|
||||
max_players=max_players,
|
||||
)
|
||||
|
||||
# Tournament-ID if we correspond to a co-op tournament (otherwise None)
|
||||
self.tournament_id: str | None = (
|
||||
app.coop_session_args.get('tournament_id'))
|
||||
self.tournament_id: str | None = app.coop_session_args.get(
|
||||
'tournament_id'
|
||||
)
|
||||
|
||||
self.campaign = getcampaign(app.coop_session_args['campaign'])
|
||||
self.campaign_level_name: str = app.coop_session_args['level']
|
||||
|
|
@ -159,10 +162,13 @@ class CoopSession(Session):
|
|||
# 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_level_name == 'Onslaught Training'
|
||||
and self._tutorial_activity is None
|
||||
and not self._ran_tutorial_activity):
|
||||
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.newactivity(TutorialActivity)
|
||||
|
||||
def get_custom_menu_entries(self) -> list[dict[str, Any]]:
|
||||
|
|
@ -170,6 +176,7 @@ class CoopSession(Session):
|
|||
|
||||
def on_player_leave(self, sessionplayer: ba.SessionPlayer) -> None:
|
||||
from ba._general import WeakCall
|
||||
|
||||
super().on_player_leave(sessionplayer)
|
||||
|
||||
_ba.timer(2.0, WeakCall(self._handle_empty_activity))
|
||||
|
|
@ -178,6 +185,7 @@ class CoopSession(Session):
|
|||
"""Handle cases where all players have left the current activity."""
|
||||
|
||||
from ba._gameactivity import GameActivity
|
||||
|
||||
activity = self.getactivity()
|
||||
if activity is None:
|
||||
return # Hmm what should we do in this case?
|
||||
|
|
@ -212,17 +220,21 @@ class CoopSession(Session):
|
|||
activity.end_game()
|
||||
|
||||
def _on_tournament_restart_menu_press(
|
||||
self, resume_callback: Callable[[], Any]) -> None:
|
||||
self, resume_callback: Callable[[], Any]
|
||||
) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.tournamententry import TournamentEntryWindow
|
||||
from ba._gameactivity import GameActivity
|
||||
|
||||
activity = self.getactivity()
|
||||
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,
|
||||
tournament_activity=activity,
|
||||
on_close_call=resume_callback)
|
||||
TournamentEntryWindow(
|
||||
tournament_id=self.tournament_id,
|
||||
tournament_activity=activity,
|
||||
on_close_call=resume_callback,
|
||||
)
|
||||
|
||||
def restart(self) -> None:
|
||||
"""Restart the current game activity."""
|
||||
|
|
@ -309,9 +321,11 @@ class CoopSession(Session):
|
|||
# Special case: if we're coming from a joining-activity
|
||||
# and will be going into onslaught-training, show the
|
||||
# tutorial first.
|
||||
if (isinstance(activity, JoinActivity)
|
||||
and self.campaign_level_name == 'Onslaught Training'
|
||||
and not (app.demo_mode or app.arcade_mode)):
|
||||
if (
|
||||
isinstance(activity, JoinActivity)
|
||||
and self.campaign_level_name == 'Onslaught Training'
|
||||
and not (app.demo_mode or app.arcade_mode)
|
||||
):
|
||||
if self._tutorial_activity is None:
|
||||
raise RuntimeError('Tutorial not preloaded properly.')
|
||||
self.setactivity(self._tutorial_activity)
|
||||
|
|
@ -336,20 +350,22 @@ class CoopSession(Session):
|
|||
|
||||
if not (app.demo_mode or app.arcade_mode):
|
||||
if self.tournament_id is not None:
|
||||
self._custom_menu_ui = [{
|
||||
'label':
|
||||
Lstr(resource='restartText'),
|
||||
'resume_on_call':
|
||||
False,
|
||||
'call':
|
||||
WeakCall(self._on_tournament_restart_menu_press
|
||||
)
|
||||
}]
|
||||
self._custom_menu_ui = [
|
||||
{
|
||||
'label': Lstr(resource='restartText'),
|
||||
'resume_on_call': False,
|
||||
'call': WeakCall(
|
||||
self._on_tournament_restart_menu_press
|
||||
),
|
||||
}
|
||||
]
|
||||
else:
|
||||
self._custom_menu_ui = [{
|
||||
'label': Lstr(resource='restartText'),
|
||||
'call': WeakCall(self.restart)
|
||||
}]
|
||||
self._custom_menu_ui = [
|
||||
{
|
||||
'label': Lstr(resource='restartText'),
|
||||
'call': WeakCall(self.restart),
|
||||
}
|
||||
]
|
||||
|
||||
# If we were in a tutorial, just pop a transition to get to the
|
||||
# actual round.
|
||||
|
|
@ -368,10 +384,13 @@ class CoopSession(Session):
|
|||
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):
|
||||
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.
|
||||
|
|
@ -391,12 +410,21 @@ class CoopSession(Session):
|
|||
else:
|
||||
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)
|
||||
score_order = (results['score_order']
|
||||
if 'score_order' in results else 'increasing')
|
||||
activity_score_type = (activity.get_score_type() if isinstance(
|
||||
activity, CoopGameActivity) else None)
|
||||
fail_message = (
|
||||
results['fail_message']
|
||||
if 'fail_message' in results
|
||||
else None
|
||||
)
|
||||
score_order = (
|
||||
results['score_order']
|
||||
if 'score_order' in results
|
||||
else 'increasing'
|
||||
)
|
||||
activity_score_type = (
|
||||
activity.get_score_type()
|
||||
if isinstance(activity, CoopGameActivity)
|
||||
else None
|
||||
)
|
||||
assert activity_score_type is not None
|
||||
scoretype = activity_score_type
|
||||
|
||||
|
|
@ -414,7 +442,8 @@ class CoopSession(Session):
|
|||
else:
|
||||
self.setactivity(
|
||||
_ba.newactivity(
|
||||
CoopScoreScreen, {
|
||||
CoopScoreScreen,
|
||||
{
|
||||
'playerinfos': playerinfos,
|
||||
'score': score,
|
||||
'fail_message': fail_message,
|
||||
|
|
@ -422,8 +451,10 @@ class CoopSession(Session):
|
|||
'score_type': scoretype,
|
||||
'outcome': outcome,
|
||||
'campaign': self.campaign,
|
||||
'level': self.campaign_level_name
|
||||
}))
|
||||
'level': self.campaign_level_name,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
# No matter what, get the next 2 levels ready to go.
|
||||
self._update_on_deck_game_instances()
|
||||
|
|
|
|||
26
dist/ba_data/python/ba/_dependency.py
vendored
26
dist/ba_data/python/ba/_dependency.py
vendored
|
|
@ -5,7 +5,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import weakref
|
||||
from typing import (Generic, TypeVar, TYPE_CHECKING)
|
||||
from typing import Generic, TypeVar, TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
|
||||
|
|
@ -44,6 +44,7 @@ class Dependency(Generic[T]):
|
|||
def get_hash(self) -> int:
|
||||
"""Return the dependency's hash, calculating it if necessary."""
|
||||
from efro.util import make_hash
|
||||
|
||||
if self._hash is None:
|
||||
self._hash = make_hash((self.cls, self.config))
|
||||
return self._hash
|
||||
|
|
@ -52,10 +53,12 @@ class Dependency(Generic[T]):
|
|||
if not isinstance(obj, DependencyComponent):
|
||||
if obj is None:
|
||||
raise TypeError(
|
||||
'Dependency must be accessed through an instance.')
|
||||
'Dependency must be accessed through an instance.'
|
||||
)
|
||||
raise TypeError(
|
||||
f'Dependency cannot be added to class of type {type(obj)}'
|
||||
' (class must inherit from ba.DependencyComponent).')
|
||||
' (class must inherit from ba.DependencyComponent).'
|
||||
)
|
||||
|
||||
# We expect to be instantiated from an already living
|
||||
# DependencyComponent with valid dep-data in place..
|
||||
|
|
@ -73,7 +76,8 @@ class Dependency(Generic[T]):
|
|||
|
||||
if not depset.resolved:
|
||||
raise RuntimeError(
|
||||
"Can't access data on an unresolved DependencySet.")
|
||||
"Can't access data on an unresolved DependencySet."
|
||||
)
|
||||
|
||||
# Look up the data in the set based on the hash for this Dependency.
|
||||
assert self._hash in depset.entries
|
||||
|
|
@ -157,8 +161,10 @@ class DependencyEntry:
|
|||
component = self.component
|
||||
assert isinstance(component, self.cls)
|
||||
if component is None:
|
||||
raise RuntimeError(f'Accessing DependencyComponent {self.cls} '
|
||||
'in an invalid state.')
|
||||
raise RuntimeError(
|
||||
f'Accessing DependencyComponent {self.cls} '
|
||||
'in an invalid state.'
|
||||
)
|
||||
return component
|
||||
|
||||
|
||||
|
|
@ -209,6 +215,7 @@ class DependencySet(Generic[T]):
|
|||
]
|
||||
if missing:
|
||||
from ba._error import DependencyError
|
||||
|
||||
raise DependencyError(missing)
|
||||
|
||||
self._resolved = True
|
||||
|
|
@ -278,7 +285,8 @@ class DependencySet(Generic[T]):
|
|||
|
||||
# Grab all Dependency instances we find in the class.
|
||||
subdeps = [
|
||||
cls for cls in dep.cls.__dict__.values()
|
||||
cls
|
||||
for cls in dep.cls.__dict__.values()
|
||||
if isinstance(cls, Dependency)
|
||||
]
|
||||
|
||||
|
|
@ -395,6 +403,7 @@ def test_depset() -> None:
|
|||
|
||||
def doit() -> None:
|
||||
from ba._error import DependencyError
|
||||
|
||||
depset = DependencySet(Dependency(TestClass))
|
||||
try:
|
||||
depset.resolve()
|
||||
|
|
@ -404,7 +413,8 @@ def test_depset() -> None:
|
|||
print('MISSING ASSET PACKAGE', dep.config)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f'Unknown dependency error for {dep.cls}') from exc
|
||||
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:
|
||||
|
|
|
|||
19
dist/ba_data/python/ba/_dualteamsession.py
vendored
19
dist/ba_data/python/ba/_dualteamsession.py
vendored
|
|
@ -33,10 +33,11 @@ class DualTeamSession(MultiTeamSession):
|
|||
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.dualteamscore import TeamVictoryScoreScreenActivity
|
||||
from bastd.activity.multiteamvictory import (
|
||||
TeamSeriesVictoryScoreScreenActivity)
|
||||
TeamSeriesVictoryScoreScreenActivity,
|
||||
)
|
||||
|
||||
winnergroups = results.winnergroups
|
||||
|
||||
# If everyone has the same score, call it a draw.
|
||||
|
|
@ -49,9 +50,13 @@ class DualTeamSession(MultiTeamSession):
|
|||
# If a team has won, show final victory screen.
|
||||
if winner.customdata['score'] >= (self._series_length - 1) / 2 + 1:
|
||||
self.setactivity(
|
||||
_ba.newactivity(TeamSeriesVictoryScoreScreenActivity,
|
||||
{'winner': winner}))
|
||||
_ba.newactivity(
|
||||
TeamSeriesVictoryScoreScreenActivity, {'winner': winner}
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.setactivity(
|
||||
_ba.newactivity(TeamVictoryScoreScreenActivity,
|
||||
{'winner': winner}))
|
||||
_ba.newactivity(
|
||||
TeamVictoryScoreScreenActivity, {'winner': winner}
|
||||
)
|
||||
)
|
||||
|
|
|
|||
2
dist/ba_data/python/ba/_error.py
vendored
2
dist/ba_data/python/ba/_error.py
vendored
|
|
@ -141,6 +141,7 @@ def print_exception(*args: Any, **keywds: Any) -> None:
|
|||
one time from an exact calling location.
|
||||
"""
|
||||
import traceback
|
||||
|
||||
if keywds:
|
||||
allowed_keywds = ['once']
|
||||
if any(keywd not in allowed_keywds for keywd in keywds):
|
||||
|
|
@ -181,6 +182,7 @@ def print_error(err_str: str, once: bool = False) -> None:
|
|||
one time from an exact calling location.
|
||||
"""
|
||||
import traceback
|
||||
|
||||
try:
|
||||
# If we're only printing once and already have, bail.
|
||||
if once:
|
||||
|
|
|
|||
45
dist/ba_data/python/ba/_freeforallsession.py
vendored
45
dist/ba_data/python/ba/_freeforallsession.py
vendored
|
|
@ -18,6 +18,7 @@ class FreeForAllSession(MultiTeamSession):
|
|||
|
||||
Category: **Gameplay Classes**
|
||||
"""
|
||||
|
||||
use_teams = False
|
||||
use_team_colors = False
|
||||
_playlist_selection_var = 'Free-for-All Playlist Selection'
|
||||
|
|
@ -55,42 +56,54 @@ class FreeForAllSession(MultiTeamSession):
|
|||
from efro.util import asserttype
|
||||
from bastd.activity.drawscore import DrawScoreScreenActivity
|
||||
from bastd.activity.multiteamvictory import (
|
||||
TeamSeriesVictoryScoreScreenActivity)
|
||||
TeamSeriesVictoryScoreScreenActivity,
|
||||
)
|
||||
from bastd.activity.freeforallvictory import (
|
||||
FreeForAllVictoryScoreScreenActivity)
|
||||
FreeForAllVictoryScoreScreenActivity,
|
||||
)
|
||||
|
||||
winners = results.winnergroups
|
||||
|
||||
# If there's multiple players and everyone has the same score,
|
||||
# call it a draw.
|
||||
if len(self.sessionplayers) > 1 and len(winners) < 2:
|
||||
self.setactivity(
|
||||
_ba.newactivity(DrawScoreScreenActivity, {'results': results}))
|
||||
_ba.newactivity(DrawScoreScreenActivity, {'results': results})
|
||||
)
|
||||
else:
|
||||
# Award different point amounts based on number of players.
|
||||
point_awards = self.get_ffa_point_awards()
|
||||
|
||||
for i, winner in enumerate(winners):
|
||||
for team in winner.teams:
|
||||
points = (point_awards[i] if i in point_awards else 0)
|
||||
team.customdata['previous_score'] = (
|
||||
team.customdata['score'])
|
||||
points = point_awards[i] if i in point_awards else 0
|
||||
team.customdata['previous_score'] = team.customdata['score']
|
||||
team.customdata['score'] += points
|
||||
|
||||
series_winners = [
|
||||
team for team in self.sessionteams
|
||||
team
|
||||
for team in self.sessionteams
|
||||
if team.customdata['score'] >= self._ffa_series_length
|
||||
]
|
||||
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].customdata['score'] !=
|
||||
series_winners[1].customdata['score'])):
|
||||
key=lambda t: asserttype(t.customdata['score'], int),
|
||||
)
|
||||
if len(series_winners) == 1 or (
|
||||
len(series_winners) > 1
|
||||
and series_winners[0].customdata['score']
|
||||
!= series_winners[1].customdata['score']
|
||||
):
|
||||
self.setactivity(
|
||||
_ba.newactivity(TeamSeriesVictoryScoreScreenActivity,
|
||||
{'winner': series_winners[0]}))
|
||||
_ba.newactivity(
|
||||
TeamSeriesVictoryScoreScreenActivity,
|
||||
{'winner': series_winners[0]},
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.setactivity(
|
||||
_ba.newactivity(FreeForAllVictoryScoreScreenActivity,
|
||||
{'results': results}))
|
||||
_ba.newactivity(
|
||||
FreeForAllVictoryScoreScreenActivity,
|
||||
{'results': results},
|
||||
)
|
||||
)
|
||||
|
|
|
|||
737
dist/ba_data/python/ba/_gameactivity.py
vendored
737
dist/ba_data/python/ba/_gameactivity.py
vendored
File diff suppressed because it is too large
Load diff
38
dist/ba_data/python/ba/_gameresults.py
vendored
38
dist/ba_data/python/ba/_gameresults.py
vendored
|
|
@ -19,6 +19,7 @@ if TYPE_CHECKING:
|
|||
@dataclass
|
||||
class WinnerGroup:
|
||||
"""Entry for a winning team or teams calculated by game-results."""
|
||||
|
||||
score: int | None
|
||||
teams: Sequence[ba.SessionTeam]
|
||||
|
||||
|
|
@ -35,8 +36,9 @@ class GameResults:
|
|||
|
||||
def __init__(self) -> None:
|
||||
self._game_set = False
|
||||
self._scores: dict[int, tuple[weakref.ref[ba.SessionTeam],
|
||||
int | None]] = {}
|
||||
self._scores: dict[
|
||||
int, tuple[weakref.ref[ba.SessionTeam], int | None]
|
||||
] = {}
|
||||
self._sessionteams: list[weakref.ref[ba.SessionTeam]] | None = None
|
||||
self._playerinfos: list[ba.PlayerInfo] | None = None
|
||||
self._lower_is_better: bool | None = None
|
||||
|
|
@ -96,8 +98,7 @@ class GameResults:
|
|||
"""Return whether there is a score for a given session-team."""
|
||||
return any(s[0]() is sessionteam for s in self._scores.values())
|
||||
|
||||
def get_sessionteam_score_str(self,
|
||||
sessionteam: ba.SessionTeam) -> ba.Lstr:
|
||||
def get_sessionteam_score_str(self, sessionteam: ba.SessionTeam) -> ba.Lstr:
|
||||
"""Return the score for the given session-team as an Lstr.
|
||||
|
||||
(properly formatted for the score type.)
|
||||
|
|
@ -106,6 +107,7 @@ class GameResults:
|
|||
from ba._language import Lstr
|
||||
from ba._generated.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()):
|
||||
|
|
@ -113,13 +115,15 @@ class GameResults:
|
|||
if score[1] is None:
|
||||
return Lstr(value='-')
|
||||
if self._scoretype is ScoreType.SECONDS:
|
||||
return timestring(score[1] * 1000,
|
||||
centi=False,
|
||||
timeformat=TimeFormat.MILLISECONDS)
|
||||
return timestring(
|
||||
score[1] * 1000,
|
||||
centi=False,
|
||||
timeformat=TimeFormat.MILLISECONDS,
|
||||
)
|
||||
if self._scoretype is ScoreType.MILLISECONDS:
|
||||
return timestring(score[1],
|
||||
centi=True,
|
||||
timeformat=TimeFormat.MILLISECONDS)
|
||||
return timestring(
|
||||
score[1], centi=True, timeformat=TimeFormat.MILLISECONDS
|
||||
)
|
||||
return Lstr(value=str(score[1]))
|
||||
return Lstr(value='-')
|
||||
|
||||
|
|
@ -174,7 +178,8 @@ class GameResults:
|
|||
# Group by best scoring teams.
|
||||
winners: dict[int, list[ba.SessionTeam]] = {}
|
||||
scores = [
|
||||
score for score in self._scores.values()
|
||||
score
|
||||
for score in self._scores.values()
|
||||
if score[0]() is not None and score[1] is not None
|
||||
]
|
||||
for score in scores:
|
||||
|
|
@ -183,10 +188,13 @@ class GameResults:
|
|||
team = score[0]()
|
||||
assert team is not None
|
||||
sval.append(team)
|
||||
results: list[tuple[int | None,
|
||||
list[ba.SessionTeam]]] = list(winners.items())
|
||||
results.sort(reverse=not self._lower_is_better,
|
||||
key=lambda x: asserttype(x[0], int))
|
||||
results: list[tuple[int | None, 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_sessionteams: list[ba.SessionTeam] = []
|
||||
|
|
|
|||
321
dist/ba_data/python/ba/_gameutils.py
vendored
321
dist/ba_data/python/ba/_gameutils.py
vendored
|
|
@ -21,7 +21,7 @@ TROPHY_CHARS = {
|
|||
'3': SpecialChar.TROPHY3,
|
||||
'0a': SpecialChar.TROPHY0A,
|
||||
'0b': SpecialChar.TROPHY0B,
|
||||
'4': SpecialChar.TROPHY4
|
||||
'4': SpecialChar.TROPHY4,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -31,6 +31,7 @@ class GameTip:
|
|||
|
||||
Category: **Gameplay Classes**
|
||||
"""
|
||||
|
||||
text: str
|
||||
icon: ba.Texture | None = None
|
||||
sound: ba.Sound | None = None
|
||||
|
|
@ -43,14 +44,16 @@ def get_trophy_string(trophy_id: str) -> str:
|
|||
return '?'
|
||||
|
||||
|
||||
def animate(node: ba.Node,
|
||||
attr: str,
|
||||
keys: dict[float, float],
|
||||
loop: bool = False,
|
||||
offset: float = 0,
|
||||
timetype: ba.TimeType = TimeType.SIM,
|
||||
timeformat: ba.TimeFormat = TimeFormat.SECONDS,
|
||||
suppress_format_warning: bool = False) -> ba.Node:
|
||||
def animate(
|
||||
node: ba.Node,
|
||||
attr: str,
|
||||
keys: dict[float, float],
|
||||
loop: bool = False,
|
||||
offset: float = 0,
|
||||
timetype: ba.TimeType = TimeType.SIM,
|
||||
timeformat: ba.TimeFormat = TimeFormat.SECONDS,
|
||||
suppress_format_warning: bool = False,
|
||||
) -> ba.Node:
|
||||
"""Animate values on a target ba.Node.
|
||||
|
||||
Category: **Gameplay Functions**
|
||||
|
|
@ -76,9 +79,11 @@ def animate(node: ba.Node,
|
|||
for item in items:
|
||||
_ba.time_format_check(timeformat, item[0])
|
||||
|
||||
curve = _ba.newnode('animcurve',
|
||||
owner=node,
|
||||
name='Driving ' + str(node) + ' \'' + attr + '\'')
|
||||
curve = _ba.newnode(
|
||||
'animcurve',
|
||||
owner=node,
|
||||
name='Driving ' + str(node) + ' \'' + attr + '\'',
|
||||
)
|
||||
|
||||
if timeformat is TimeFormat.SECONDS:
|
||||
mult = 1000
|
||||
|
|
@ -89,7 +94,8 @@ def animate(node: ba.Node,
|
|||
|
||||
curve.times = [int(mult * time) for time, val in items]
|
||||
curve.offset = _ba.time(timeformat=TimeFormat.MILLISECONDS) + int(
|
||||
mult * offset)
|
||||
mult * offset
|
||||
)
|
||||
curve.values = [val for time, val in items]
|
||||
curve.loop = loop
|
||||
|
||||
|
|
@ -99,9 +105,11 @@ def animate(node: ba.Node,
|
|||
# get disconnected.
|
||||
if not loop:
|
||||
# noinspection PyUnresolvedReferences
|
||||
_ba.timer(int(mult * items[-1][0]) + 1000,
|
||||
curve.delete,
|
||||
timeformat=TimeFormat.MILLISECONDS)
|
||||
_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.
|
||||
|
|
@ -117,15 +125,17 @@ def animate(node: ba.Node,
|
|||
return curve
|
||||
|
||||
|
||||
def animate_array(node: ba.Node,
|
||||
attr: str,
|
||||
size: int,
|
||||
keys: dict[float, Sequence[float]],
|
||||
loop: bool = False,
|
||||
offset: float = 0,
|
||||
timetype: ba.TimeType = TimeType.SIM,
|
||||
timeformat: ba.TimeFormat = TimeFormat.SECONDS,
|
||||
suppress_format_warning: bool = False) -> None:
|
||||
def animate_array(
|
||||
node: ba.Node,
|
||||
attr: str,
|
||||
size: int,
|
||||
keys: dict[float, Sequence[float]],
|
||||
loop: bool = False,
|
||||
offset: float = 0,
|
||||
timetype: ba.TimeType = TimeType.SIM,
|
||||
timeformat: ba.TimeFormat = TimeFormat.SECONDS,
|
||||
suppress_format_warning: bool = False,
|
||||
) -> None:
|
||||
"""Animate an array of values on a target ba.Node.
|
||||
|
||||
Category: **Gameplay Functions**
|
||||
|
|
@ -163,15 +173,19 @@ def animate_array(node: ba.Node,
|
|||
globalsnode = _ba.getsession().sessionglobalsnode
|
||||
|
||||
for i in range(size):
|
||||
curve = _ba.newnode('animcurve',
|
||||
owner=node,
|
||||
name=('Driving ' + str(node) + ' \'' + attr +
|
||||
'\' member ' + str(i)))
|
||||
curve = _ba.newnode(
|
||||
'animcurve',
|
||||
owner=node,
|
||||
name=(
|
||||
'Driving ' + str(node) + ' \'' + attr + '\' member ' + str(i)
|
||||
),
|
||||
)
|
||||
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(
|
||||
mult * offset)
|
||||
mult * offset
|
||||
)
|
||||
curve.loop = loop
|
||||
curve.connectattr('out', combine, 'input' + str(i))
|
||||
|
||||
|
|
@ -180,9 +194,11 @@ def animate_array(node: ba.Node,
|
|||
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)
|
||||
_ba.timer(
|
||||
int(mult * items[-1][0]) + 1000,
|
||||
curve.delete,
|
||||
timeformat=TimeFormat.MILLISECONDS,
|
||||
)
|
||||
combine.connectattr('output', node, attr)
|
||||
|
||||
# If we're not looping, set a timer to kill the combine once
|
||||
|
|
@ -192,13 +208,16 @@ def animate_array(node: ba.Node,
|
|||
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)
|
||||
_ba.timer(
|
||||
int(mult * items[-1][0]) + 1000,
|
||||
combine.delete,
|
||||
timeformat=TimeFormat.MILLISECONDS,
|
||||
)
|
||||
|
||||
|
||||
def show_damage_count(damage: str, position: Sequence[float],
|
||||
direction: Sequence[float]) -> None:
|
||||
def show_damage_count(
|
||||
damage: str, position: Sequence[float], direction: Sequence[float]
|
||||
) -> None:
|
||||
"""Pop up a damage count at a position in space.
|
||||
|
||||
Category: **Gameplay Functions**
|
||||
|
|
@ -210,16 +229,18 @@ def show_damage_count(damage: str, position: Sequence[float],
|
|||
# (connected clients may have differing configs so they won't
|
||||
# get the intended results).
|
||||
do_big = app.ui.uiscale is UIScale.SMALL or app.vr_mode
|
||||
txtnode = _ba.newnode('text',
|
||||
attrs={
|
||||
'text': damage,
|
||||
'in_world': True,
|
||||
'h_align': 'center',
|
||||
'flatness': 1.0,
|
||||
'shadow': 1.0 if do_big else 0.7,
|
||||
'color': (1, 0.25, 0.25, 1),
|
||||
'scale': 0.015 if do_big else 0.01
|
||||
})
|
||||
txtnode = _ba.newnode(
|
||||
'text',
|
||||
attrs={
|
||||
'text': damage,
|
||||
'in_world': True,
|
||||
'h_align': 'center',
|
||||
'flatness': 1.0,
|
||||
'shadow': 1.0 if do_big else 0.7,
|
||||
'color': (1, 0.25, 0.25, 1),
|
||||
'scale': 0.015 if do_big else 0.01,
|
||||
},
|
||||
)
|
||||
# Translate upward.
|
||||
tcombine = _ba.newnode('combine', owner=txtnode, attrs={'size': 3})
|
||||
tcombine.connectattr('output', txtnode, 'position')
|
||||
|
|
@ -233,27 +254,35 @@ def show_damage_count(damage: str, position: Sequence[float],
|
|||
vval *= 0.5
|
||||
p_start = position[0]
|
||||
p_dir = direction[0]
|
||||
animate(tcombine, 'input0',
|
||||
{i[0] * lifespan: p_start + p_dir * i[1]
|
||||
for i in v_vals})
|
||||
animate(
|
||||
tcombine,
|
||||
'input0',
|
||||
{i[0] * lifespan: p_start + p_dir * i[1] for i in v_vals},
|
||||
)
|
||||
p_start = position[1]
|
||||
p_dir = direction[1]
|
||||
animate(tcombine, 'input1',
|
||||
{i[0] * lifespan: p_start + p_dir * i[1]
|
||||
for i in v_vals})
|
||||
animate(
|
||||
tcombine,
|
||||
'input1',
|
||||
{i[0] * lifespan: p_start + p_dir * i[1] for i in v_vals},
|
||||
)
|
||||
p_start = position[2]
|
||||
p_dir = direction[2]
|
||||
animate(tcombine, 'input2',
|
||||
{i[0] * lifespan: p_start + p_dir * i[1]
|
||||
for i in v_vals})
|
||||
animate(
|
||||
tcombine,
|
||||
'input2',
|
||||
{i[0] * lifespan: p_start + p_dir * i[1] for i in v_vals},
|
||||
)
|
||||
animate(txtnode, 'opacity', {0.7 * lifespan: 1.0, lifespan: 0.0})
|
||||
_ba.timer(lifespan, txtnode.delete)
|
||||
|
||||
|
||||
def timestring(timeval: float,
|
||||
centi: bool = True,
|
||||
timeformat: ba.TimeFormat = TimeFormat.SECONDS,
|
||||
suppress_format_warning: bool = False) -> ba.Lstr:
|
||||
def timestring(
|
||||
timeval: float,
|
||||
centi: bool = True,
|
||||
timeformat: ba.TimeFormat = TimeFormat.SECONDS,
|
||||
suppress_format_warning: bool = False,
|
||||
) -> ba.Lstr:
|
||||
"""Generate a ba.Lstr for displaying a time value.
|
||||
|
||||
Category: **General Utility Functions**
|
||||
|
|
@ -292,32 +321,56 @@ def timestring(timeval: float,
|
|||
hval = (timeval // 1000) // (60 * 60)
|
||||
if hval != 0:
|
||||
bits.append('${H}')
|
||||
subs.append(('${H}',
|
||||
Lstr(resource='timeSuffixHoursText',
|
||||
subs=[('${COUNT}', str(hval))])))
|
||||
subs.append(
|
||||
(
|
||||
'${H}',
|
||||
Lstr(
|
||||
resource='timeSuffixHoursText',
|
||||
subs=[('${COUNT}', str(hval))],
|
||||
),
|
||||
)
|
||||
)
|
||||
mval = ((timeval // 1000) // 60) % 60
|
||||
if mval != 0:
|
||||
bits.append('${M}')
|
||||
subs.append(('${M}',
|
||||
Lstr(resource='timeSuffixMinutesText',
|
||||
subs=[('${COUNT}', str(mval))])))
|
||||
subs.append(
|
||||
(
|
||||
'${M}',
|
||||
Lstr(
|
||||
resource='timeSuffixMinutesText',
|
||||
subs=[('${COUNT}', str(mval))],
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
# We add seconds if its non-zero *or* we haven't added anything else.
|
||||
if centi:
|
||||
# pylint: disable=consider-using-f-string
|
||||
sval = (timeval / 1000.0 % 60.0)
|
||||
sval = timeval / 1000.0 % 60.0
|
||||
if sval >= 0.005 or not bits:
|
||||
bits.append('${S}')
|
||||
subs.append(('${S}',
|
||||
Lstr(resource='timeSuffixSecondsText',
|
||||
subs=[('${COUNT}', ('%.2f' % sval))])))
|
||||
subs.append(
|
||||
(
|
||||
'${S}',
|
||||
Lstr(
|
||||
resource='timeSuffixSecondsText',
|
||||
subs=[('${COUNT}', ('%.2f' % sval))],
|
||||
),
|
||||
)
|
||||
)
|
||||
else:
|
||||
sval = (timeval // 1000 % 60)
|
||||
sval = timeval // 1000 % 60
|
||||
if sval != 0 or not bits:
|
||||
bits.append('${S}')
|
||||
subs.append(('${S}',
|
||||
Lstr(resource='timeSuffixSecondsText',
|
||||
subs=[('${COUNT}', str(sval))])))
|
||||
subs.append(
|
||||
(
|
||||
'${S}',
|
||||
Lstr(
|
||||
resource='timeSuffixSecondsText',
|
||||
subs=[('${COUNT}', str(sval))],
|
||||
),
|
||||
)
|
||||
)
|
||||
return Lstr(value=' '.join(bits), subs=subs)
|
||||
|
||||
|
||||
|
|
@ -332,11 +385,17 @@ def cameraflash(duration: float = 999.0) -> None:
|
|||
# pylint: disable=too-many-locals
|
||||
import random
|
||||
from ba._nodeactor import NodeActor
|
||||
|
||||
x_spread = 10
|
||||
y_spread = 5
|
||||
positions = [[-x_spread, -y_spread], [0, -y_spread], [0, y_spread],
|
||||
[x_spread, -y_spread], [x_spread, y_spread],
|
||||
[-x_spread, y_spread]]
|
||||
positions = [
|
||||
[-x_spread, -y_spread],
|
||||
[0, -y_spread],
|
||||
[0, y_spread],
|
||||
[x_spread, -y_spread],
|
||||
[x_spread, y_spread],
|
||||
[-x_spread, y_spread],
|
||||
]
|
||||
times = [0, 2700, 1000, 1800, 500, 1400]
|
||||
|
||||
# Store this on the current activity so we only have one at a time.
|
||||
|
|
@ -345,57 +404,73 @@ def cameraflash(duration: float = 999.0) -> None:
|
|||
activity.camera_flash_data = [] # type: ignore
|
||||
for i in range(6):
|
||||
light = NodeActor(
|
||||
_ba.newnode('light',
|
||||
attrs={
|
||||
'position': (positions[i][0], 0, positions[i][1]),
|
||||
'radius': 1.0,
|
||||
'lights_volumes': False,
|
||||
'height_attenuated': False,
|
||||
'color': (0.2, 0.2, 0.8)
|
||||
}))
|
||||
_ba.newnode(
|
||||
'light',
|
||||
attrs={
|
||||
'position': (positions[i][0], 0, positions[i][1]),
|
||||
'radius': 1.0,
|
||||
'lights_volumes': False,
|
||||
'height_attenuated': False,
|
||||
'color': (0.2, 0.2, 0.8),
|
||||
},
|
||||
)
|
||||
)
|
||||
sval = 1.87
|
||||
iscale = 1.3
|
||||
tcombine = _ba.newnode('combine',
|
||||
owner=light.node,
|
||||
attrs={
|
||||
'size': 3,
|
||||
'input0': positions[i][0],
|
||||
'input1': 0,
|
||||
'input2': positions[i][1]
|
||||
})
|
||||
tcombine = _ba.newnode(
|
||||
'combine',
|
||||
owner=light.node,
|
||||
attrs={
|
||||
'size': 3,
|
||||
'input0': positions[i][0],
|
||||
'input1': 0,
|
||||
'input2': positions[i][1],
|
||||
},
|
||||
)
|
||||
assert light.node
|
||||
tcombine.connectattr('output', light.node, 'position')
|
||||
xval = positions[i][0]
|
||||
yval = positions[i][1]
|
||||
spd = 0.5 + random.random()
|
||||
spd2 = 0.5 + random.random()
|
||||
animate(tcombine,
|
||||
'input0', {
|
||||
0.0: xval + 0,
|
||||
0.069 * spd: xval + 10.0,
|
||||
0.143 * spd: xval - 10.0,
|
||||
0.201 * spd: xval + 0
|
||||
},
|
||||
loop=True)
|
||||
animate(tcombine,
|
||||
'input2', {
|
||||
0.0: yval + 0,
|
||||
0.15 * spd2: yval + 10.0,
|
||||
0.287 * spd2: yval - 10.0,
|
||||
0.398 * spd2: yval + 0
|
||||
},
|
||||
loop=True)
|
||||
animate(light.node,
|
||||
'intensity', {
|
||||
0.0: 0,
|
||||
0.02 * sval: 0,
|
||||
0.05 * sval: 0.8 * iscale,
|
||||
0.08 * sval: 0,
|
||||
0.1 * sval: 0
|
||||
},
|
||||
loop=True,
|
||||
offset=times[i])
|
||||
_ba.timer((times[i] + random.randint(1, int(duration)) * 40 * sval),
|
||||
light.node.delete,
|
||||
timeformat=TimeFormat.MILLISECONDS)
|
||||
animate(
|
||||
tcombine,
|
||||
'input0',
|
||||
{
|
||||
0.0: xval + 0,
|
||||
0.069 * spd: xval + 10.0,
|
||||
0.143 * spd: xval - 10.0,
|
||||
0.201 * spd: xval + 0,
|
||||
},
|
||||
loop=True,
|
||||
)
|
||||
animate(
|
||||
tcombine,
|
||||
'input2',
|
||||
{
|
||||
0.0: yval + 0,
|
||||
0.15 * spd2: yval + 10.0,
|
||||
0.287 * spd2: yval - 10.0,
|
||||
0.398 * spd2: yval + 0,
|
||||
},
|
||||
loop=True,
|
||||
)
|
||||
animate(
|
||||
light.node,
|
||||
'intensity',
|
||||
{
|
||||
0.0: 0,
|
||||
0.02 * sval: 0,
|
||||
0.05 * sval: 0.8 * iscale,
|
||||
0.08 * sval: 0,
|
||||
0.1 * sval: 0,
|
||||
},
|
||||
loop=True,
|
||||
offset=times[i],
|
||||
)
|
||||
_ba.timer(
|
||||
(times[i] + random.randint(1, int(duration)) * 40 * sval),
|
||||
light.node.delete,
|
||||
timeformat=TimeFormat.MILLISECONDS,
|
||||
)
|
||||
activity.camera_flash_data.append(light) # type: ignore
|
||||
|
|
|
|||
127
dist/ba_data/python/ba/_general.py
vendored
127
dist/ba_data/python/ba/_general.py
vendored
|
|
@ -3,7 +3,6 @@
|
|||
"""Utility snippets applying to generic Python code."""
|
||||
from __future__ import annotations
|
||||
|
||||
import gc
|
||||
import types
|
||||
import weakref
|
||||
import random
|
||||
|
|
@ -16,7 +15,6 @@ from ba._error import print_error, print_exception
|
|||
from ba._generated.enums import TimeType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from types import FrameType
|
||||
from typing import Any
|
||||
from efro.call import Call as Call # 'as Call' so we re-export.
|
||||
|
||||
|
|
@ -64,6 +62,7 @@ def getclass(name: str, subclassof: type[T]) -> type[T]:
|
|||
'subclassof' class, and a TypeError will be raised if not.
|
||||
"""
|
||||
import importlib
|
||||
|
||||
splits = name.split('.')
|
||||
modulename = '.'.join(splits[:-1])
|
||||
classname = splits[-1]
|
||||
|
|
@ -84,8 +83,10 @@ def json_prep(data: Any) -> Any:
|
|||
"""
|
||||
|
||||
if isinstance(data, dict):
|
||||
return dict((json_prep(key), json_prep(value))
|
||||
for key, value in list(data.items()))
|
||||
return dict(
|
||||
(json_prep(key), json_prep(value))
|
||||
for key, value in list(data.items())
|
||||
)
|
||||
if isinstance(data, list):
|
||||
return [json_prep(element) for element in data]
|
||||
if isinstance(data, tuple):
|
||||
|
|
@ -96,19 +97,23 @@ def json_prep(data: Any) -> Any:
|
|||
return data.decode(errors='ignore')
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
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)):
|
||||
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."""
|
||||
if isinstance(data, dict):
|
||||
return dict((utf8_all(key), utf8_all(value))
|
||||
for key, value in list(data.items()))
|
||||
return dict(
|
||||
(utf8_all(key), utf8_all(value))
|
||||
for key, value in list(data.items())
|
||||
)
|
||||
if isinstance(data, list):
|
||||
return [utf8_all(element) for element in data]
|
||||
if isinstance(data, tuple):
|
||||
|
|
@ -118,19 +123,6 @@ def utf8_all(data: Any) -> Any:
|
|||
return data
|
||||
|
||||
|
||||
def print_refs(obj: Any) -> None:
|
||||
"""Print a list of known live references to an object."""
|
||||
|
||||
# Hmmm; I just noticed that calling this on an object
|
||||
# seems to keep it alive. Should figure out why.
|
||||
print('REFERENCES FOR', obj, ':')
|
||||
refs = list(gc.get_referrers(obj))
|
||||
i = 1
|
||||
for ref in refs:
|
||||
print(' ref', i, ':', ref)
|
||||
i += 1
|
||||
|
||||
|
||||
def get_type_name(cls: type) -> str:
|
||||
"""Return a full type name including module for a class."""
|
||||
return cls.__module__ + '.' + cls.__name__
|
||||
|
|
@ -190,11 +182,17 @@ class _WeakCall:
|
|||
else:
|
||||
app = _ba.app
|
||||
if not app.did_weak_call_warning:
|
||||
print(('Warning: callable passed to ba.WeakCall() is not'
|
||||
' weak-referencable (' + str(args[0]) +
|
||||
'); use ba.Call() instead to avoid this '
|
||||
'warning. Stack-trace:'))
|
||||
print(
|
||||
(
|
||||
'Warning: callable passed to ba.WeakCall() is not'
|
||||
' weak-referencable ('
|
||||
+ str(args[0])
|
||||
+ '); use ba.Call() instead to avoid this '
|
||||
'warning. Stack-trace:'
|
||||
)
|
||||
)
|
||||
import traceback
|
||||
|
||||
traceback.print_stack()
|
||||
app.did_weak_call_warning = True
|
||||
self._call = args[0]
|
||||
|
|
@ -205,8 +203,15 @@ class _WeakCall:
|
|||
return self._call(*self._args + args_extra, **self._keywds)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return ('<ba.WeakCall object; _call=' + str(self._call) + ' _args=' +
|
||||
str(self._args) + ' _keywds=' + str(self._keywds) + '>')
|
||||
return (
|
||||
'<ba.WeakCall object; _call='
|
||||
+ str(self._call)
|
||||
+ ' _args='
|
||||
+ str(self._args)
|
||||
+ ' _keywds='
|
||||
+ str(self._keywds)
|
||||
+ '>'
|
||||
)
|
||||
|
||||
|
||||
class _Call:
|
||||
|
|
@ -244,8 +249,15 @@ class _Call:
|
|||
return self._call(*self._args + args_extra, **self._keywds)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return ('<ba.Call object; _call=' + str(self._call) + ' _args=' +
|
||||
str(self._args) + ' _keywds=' + str(self._keywds) + '>')
|
||||
return (
|
||||
'<ba.Call object; _call='
|
||||
+ str(self._call)
|
||||
+ ' _args='
|
||||
+ str(self._args)
|
||||
+ ' _keywds='
|
||||
+ str(self._keywds)
|
||||
+ '>'
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -278,7 +290,7 @@ class WeakMethod:
|
|||
obj = self._obj()
|
||||
if obj is None:
|
||||
return None
|
||||
return self._func(*((obj, ) + args), **keywds)
|
||||
return self._func(*((obj,) + args), **keywds)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return '<ba.WeakMethod object; call=' + str(self._func) + '>'
|
||||
|
|
@ -300,50 +312,9 @@ def verify_object_death(obj: object) -> None:
|
|||
# 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}')
|
||||
_ba.timer(
|
||||
delay, lambda: _verify_object_death(ref), timetype=TimeType.REAL
|
||||
)
|
||||
|
||||
|
||||
def _verify_object_death(wref: weakref.ref) -> None:
|
||||
|
|
@ -357,9 +328,11 @@ def _verify_object_death(wref: weakref.ref) -> None:
|
|||
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)
|
||||
print(
|
||||
f'{Clr.RED}Error: {name} not dying when expected to:'
|
||||
f' {Clr.BLD}{obj}{Clr.RST}\n'
|
||||
'See efro.debug for ways to debug this.'
|
||||
)
|
||||
|
||||
|
||||
def storagename(suffix: str | None = None) -> str:
|
||||
|
|
|
|||
6
dist/ba_data/python/ba/_generated/enums.py
vendored
6
dist/ba_data/python/ba/_generated/enums.py
vendored
|
|
@ -10,6 +10,7 @@ class InputType(Enum):
|
|||
Category: Enums
|
||||
|
||||
"""
|
||||
|
||||
UP_DOWN = 2
|
||||
LEFT_RIGHT = 3
|
||||
JUMP_PRESS = 4
|
||||
|
|
@ -57,6 +58,7 @@ class UIScale(Enum):
|
|||
content needs to be presented as large and clear in order to remain
|
||||
readable from an average distance.
|
||||
"""
|
||||
|
||||
LARGE = 0
|
||||
MEDIUM = 1
|
||||
SMALL = 2
|
||||
|
|
@ -79,6 +81,7 @@ class TimeType(Enum):
|
|||
not advance while the app is backgrounded for instance. (the engine
|
||||
attempts to prevent single large time jumps from occurring)
|
||||
"""
|
||||
|
||||
SIM = 0
|
||||
BASE = 1
|
||||
REAL = 2
|
||||
|
|
@ -89,6 +92,7 @@ class TimeFormat(Enum):
|
|||
|
||||
Category: Enums
|
||||
"""
|
||||
|
||||
SECONDS = 0
|
||||
MILLISECONDS = 1
|
||||
|
||||
|
|
@ -98,6 +102,7 @@ class Permission(Enum):
|
|||
|
||||
Category: Enums
|
||||
"""
|
||||
|
||||
STORAGE = 0
|
||||
|
||||
|
||||
|
|
@ -106,6 +111,7 @@ class SpecialChar(Enum):
|
|||
|
||||
Category: Enums
|
||||
"""
|
||||
|
||||
DOWN_ARROW = 0
|
||||
UP_ARROW = 1
|
||||
LEFT_ARROW = 2
|
||||
|
|
|
|||
119
dist/ba_data/python/ba/_hooks.py
vendored
119
dist/ba_data/python/ba/_hooks.py
vendored
|
|
@ -54,87 +54,113 @@ def set_config_fullscreen_off() -> None:
|
|||
|
||||
def not_signed_in_screen_message() -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
_ba.screenmessage(Lstr(resource='notSignedInErrorText'))
|
||||
|
||||
|
||||
def connecting_to_party_message() -> None:
|
||||
from ba._language import Lstr
|
||||
_ba.screenmessage(Lstr(resource='internal.connectingToPartyText'),
|
||||
color=(1, 1, 1))
|
||||
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='internal.connectingToPartyText'), color=(1, 1, 1)
|
||||
)
|
||||
|
||||
|
||||
def rejecting_invite_already_in_party_message() -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='internal.rejectingInviteAlreadyInPartyText'),
|
||||
color=(1, 0.5, 0))
|
||||
color=(1, 0.5, 0),
|
||||
)
|
||||
|
||||
|
||||
def connection_failed_message() -> None:
|
||||
from ba._language import Lstr
|
||||
_ba.screenmessage(Lstr(resource='internal.connectionFailedText'),
|
||||
color=(1, 0.5, 0))
|
||||
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='internal.connectionFailedText'), color=(1, 0.5, 0)
|
||||
)
|
||||
|
||||
|
||||
def temporarily_unavailable_message() -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='getTicketsWindow.unavailableTemporarilyText'),
|
||||
color=(1, 0, 0))
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
|
||||
|
||||
def in_progress_message() -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
_ba.screenmessage(Lstr(resource='getTicketsWindow.inProgressText'),
|
||||
color=(1, 0, 0))
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='getTicketsWindow.inProgressText'), color=(1, 0, 0)
|
||||
)
|
||||
|
||||
|
||||
def error_message() -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
_ba.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
|
||||
|
||||
|
||||
def purchase_not_valid_error() -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
_ba.screenmessage(Lstr(resource='store.purchaseNotValidError',
|
||||
subs=[('${EMAIL}', 'support@froemling.net')]),
|
||||
color=(1, 0, 0))
|
||||
_ba.screenmessage(
|
||||
Lstr(
|
||||
resource='store.purchaseNotValidError',
|
||||
subs=[('${EMAIL}', 'support@froemling.net')],
|
||||
),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
|
||||
|
||||
def purchase_already_in_progress_error() -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
_ba.screenmessage(Lstr(resource='store.purchaseAlreadyInProgressText'),
|
||||
color=(1, 0, 0))
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='store.purchaseAlreadyInProgressText'), color=(1, 0, 0)
|
||||
)
|
||||
|
||||
|
||||
def gear_vr_controller_warning() -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
_ba.screenmessage(Lstr(resource='usesExternalControllerText'),
|
||||
color=(1, 0, 0))
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='usesExternalControllerText'), color=(1, 0, 0)
|
||||
)
|
||||
|
||||
|
||||
def uuid_str() -> str:
|
||||
import uuid
|
||||
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
def orientation_reset_cb_message() -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='internal.vrOrientationResetCardboardText'),
|
||||
color=(0, 1, 0))
|
||||
color=(0, 1, 0),
|
||||
)
|
||||
|
||||
|
||||
def orientation_reset_message() -> None:
|
||||
from ba._language import Lstr
|
||||
_ba.screenmessage(Lstr(resource='internal.vrOrientationResetText'),
|
||||
color=(0, 1, 0))
|
||||
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='internal.vrOrientationResetText'), color=(0, 1, 0)
|
||||
)
|
||||
|
||||
|
||||
def on_app_pause() -> None:
|
||||
|
|
@ -147,12 +173,14 @@ def on_app_resume() -> None:
|
|||
|
||||
def launch_main_menu_session() -> None:
|
||||
from bastd.mainmenu import MainMenuSession
|
||||
|
||||
_ba.new_host_session(MainMenuSession)
|
||||
|
||||
|
||||
def language_test_toggle() -> None:
|
||||
_ba.app.lang.setlanguage('Gibberish' if _ba.app.lang.language ==
|
||||
'English' else 'English')
|
||||
_ba.app.lang.setlanguage(
|
||||
'Gibberish' if _ba.app.lang.language == 'English' else 'English'
|
||||
)
|
||||
|
||||
|
||||
def award_in_control_achievement() -> None:
|
||||
|
|
@ -173,8 +201,10 @@ def launch_coop_game(name: str) -> None:
|
|||
|
||||
def purchases_restored_message() -> None:
|
||||
from ba._language import Lstr
|
||||
_ba.screenmessage(Lstr(resource='getTicketsWindow.purchasesRestoredText'),
|
||||
color=(0, 1, 0))
|
||||
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='getTicketsWindow.purchasesRestoredText'), color=(0, 1, 0)
|
||||
)
|
||||
|
||||
|
||||
def dismiss_wii_remotes_window() -> None:
|
||||
|
|
@ -185,8 +215,10 @@ def dismiss_wii_remotes_window() -> None:
|
|||
|
||||
def unavailable_message() -> None:
|
||||
from ba._language import Lstr
|
||||
_ba.screenmessage(Lstr(resource='getTicketsWindow.unavailableText'),
|
||||
color=(1, 0, 0))
|
||||
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='getTicketsWindow.unavailableText'), color=(1, 0, 0)
|
||||
)
|
||||
|
||||
|
||||
def submit_analytics_counts(sval: str) -> None:
|
||||
|
|
@ -196,19 +228,23 @@ def submit_analytics_counts(sval: str) -> None:
|
|||
|
||||
def set_last_ad_network(sval: str) -> None:
|
||||
import time
|
||||
|
||||
_ba.app.ads.last_ad_network = sval
|
||||
_ba.app.ads.last_ad_network_set_time = time.time()
|
||||
|
||||
|
||||
def no_game_circle_message() -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
_ba.screenmessage(Lstr(resource='noGameCircleText'), color=(1, 0, 0))
|
||||
|
||||
|
||||
def google_play_purchases_not_available_message() -> None:
|
||||
from ba._language import Lstr
|
||||
_ba.screenmessage(Lstr(resource='googlePlayPurchasesNotAvailableText'),
|
||||
color=(1, 0, 0))
|
||||
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='googlePlayPurchasesNotAvailableText'), color=(1, 0, 0)
|
||||
)
|
||||
|
||||
|
||||
def empty_call() -> None:
|
||||
|
|
@ -229,8 +265,10 @@ def coin_icon_press() -> None:
|
|||
|
||||
def ticket_icon_press() -> None:
|
||||
from bastd.ui.resourcetypeinfo import ResourceTypeInfoWindow
|
||||
|
||||
ResourceTypeInfoWindow(
|
||||
origin_widget=_ba.get_special_widget('tickets_info_button'))
|
||||
origin_widget=_ba.get_special_widget('tickets_info_button')
|
||||
)
|
||||
|
||||
|
||||
def back_button_press() -> None:
|
||||
|
|
@ -243,6 +281,7 @@ def friends_button_press() -> None:
|
|||
|
||||
def print_trace() -> None:
|
||||
import traceback
|
||||
|
||||
print('Python Traceback (most recent call last):')
|
||||
traceback.print_stack()
|
||||
|
||||
|
|
@ -256,6 +295,7 @@ def toggle_fullscreen() -> None:
|
|||
def party_icon_activate(origin: Sequence[float]) -> None:
|
||||
import weakref
|
||||
from bastd.ui.party import PartyWindow
|
||||
|
||||
app = _ba.app
|
||||
_ba.playsound(_ba.getsound('swish'))
|
||||
|
||||
|
|
@ -276,13 +316,16 @@ def ui_remote_press() -> None:
|
|||
|
||||
# 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.screenmessage(
|
||||
Lstr(resource='internal.controllerForMenusOnlyText'),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
|
||||
|
||||
def quit_window() -> None:
|
||||
from bastd.ui.confirm import QuitWindow
|
||||
|
||||
QuitWindow()
|
||||
|
||||
|
||||
|
|
@ -292,6 +335,7 @@ def remove_in_game_ads_message() -> None:
|
|||
|
||||
def telnet_access_request() -> None:
|
||||
from bastd.ui.telnet import TelnetAccessRequestWindow
|
||||
|
||||
TelnetAccessRequestWindow()
|
||||
|
||||
|
||||
|
|
@ -305,11 +349,13 @@ def shutdown() -> None:
|
|||
|
||||
def gc_disable() -> None:
|
||||
import gc
|
||||
|
||||
gc.disable()
|
||||
|
||||
|
||||
def device_menu_press(device: ba.InputDevice) -> None:
|
||||
from bastd.ui.mainmenu import MainMenuWindow
|
||||
|
||||
in_main_menu = _ba.app.ui.has_main_menu_window()
|
||||
if not in_main_menu:
|
||||
_ba.set_ui_input_device(device)
|
||||
|
|
@ -319,6 +365,7 @@ def device_menu_press(device: ba.InputDevice) -> None:
|
|||
|
||||
def show_url_window(address: str) -> None:
|
||||
from bastd.ui.url import ShowURLWindow
|
||||
|
||||
ShowURLWindow(address)
|
||||
|
||||
|
||||
|
|
@ -328,8 +375,9 @@ def party_invite_revoke(invite_id: str) -> None:
|
|||
for winref in _ba.app.invite_confirm_windows:
|
||||
win = winref()
|
||||
if win is not None and win.ew_party_invite_id == invite_id:
|
||||
_ba.containerwidget(edit=win.get_root_widget(),
|
||||
transition='out_right')
|
||||
_ba.containerwidget(
|
||||
edit=win.get_root_widget(), transition='out_right'
|
||||
)
|
||||
|
||||
import custom_hooks as chooks
|
||||
def filter_chat_message(msg: str, client_id: int) -> str | None:
|
||||
|
|
@ -370,8 +418,10 @@ def on_player_leave(pb_id:str)-> None:
|
|||
|
||||
|
||||
def local_chat_message(msg: str) -> None:
|
||||
if (_ba.app.ui.party_window is not None
|
||||
and _ba.app.ui.party_window() is not None):
|
||||
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)
|
||||
|
||||
|
||||
|
|
@ -381,13 +431,14 @@ def get_player_icon(sessionplayer: ba.SessionPlayer) -> dict[str, Any]:
|
|||
'texture': _ba.gettexture(info['texture']),
|
||||
'tint_texture': _ba.gettexture(info['tint_texture']),
|
||||
'tint_color': info['tint_color'],
|
||||
'tint2_color': info['tint2_color']
|
||||
'tint2_color': info['tint2_color'],
|
||||
}
|
||||
|
||||
|
||||
def hash_strings(inputs: list[str]) -> str:
|
||||
"""Hash provided strings into a short output string."""
|
||||
import hashlib
|
||||
|
||||
sha = hashlib.sha1()
|
||||
for inp in inputs:
|
||||
sha.update(inp.encode())
|
||||
|
|
|
|||
103
dist/ba_data/python/ba/_input.py
vendored
103
dist/ba_data/python/ba/_input.py
vendored
|
|
@ -48,7 +48,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonRight': 23,
|
||||
'buttonUp': 20,
|
||||
'buttonDown': 21,
|
||||
'buttonVRReorient': 110
|
||||
'buttonVRReorient': 110,
|
||||
}.get(name, -1)
|
||||
|
||||
# If there's an entry in our config for this controller, use it.
|
||||
|
|
@ -79,7 +79,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonRun2': 5,
|
||||
'buttonRun1': 6,
|
||||
'buttonJump': 1,
|
||||
'buttonIgnored': 11
|
||||
'buttonIgnored': 11,
|
||||
}.get(name, -1)
|
||||
|
||||
# Ps4 controller.
|
||||
|
|
@ -94,7 +94,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonPunch': 1,
|
||||
'buttonRun2': 5,
|
||||
'buttonRun1': 6,
|
||||
'triggerRun1': 5
|
||||
'triggerRun1': 5,
|
||||
}.get(name, -1)
|
||||
|
||||
# Look for some exact types.
|
||||
|
|
@ -112,7 +112,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonPunch': 100,
|
||||
'buttonRun2': 103,
|
||||
'buttonRun1': 104,
|
||||
'triggerRun1': 24
|
||||
'triggerRun1': 24,
|
||||
}.get(name, -1)
|
||||
if devicename == 'NYKO PLAYPAD PRO':
|
||||
return {
|
||||
|
|
@ -126,7 +126,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonRight': 23,
|
||||
'buttonStart': 83,
|
||||
'buttonPunch': 100,
|
||||
'buttonDown': 21
|
||||
'buttonDown': 21,
|
||||
}.get(name, -1)
|
||||
if devicename == 'Logitech Dual Action':
|
||||
return {
|
||||
|
|
@ -136,7 +136,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonBomb': 101,
|
||||
'buttonJump': 100,
|
||||
'buttonStart': 109,
|
||||
'buttonPunch': 97
|
||||
'buttonPunch': 97,
|
||||
}.get(name, -1)
|
||||
if devicename == 'Xbox 360 Wireless Receiver':
|
||||
return {
|
||||
|
|
@ -150,7 +150,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonRight': 23,
|
||||
'buttonStart': 83,
|
||||
'buttonPunch': 100,
|
||||
'buttonDown': 21
|
||||
'buttonDown': 21,
|
||||
}.get(name, -1)
|
||||
if devicename == 'Microsoft X-Box 360 pad':
|
||||
return {
|
||||
|
|
@ -160,11 +160,12 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonBomb': 98,
|
||||
'buttonJump': 97,
|
||||
'buttonStart': 83,
|
||||
'buttonPunch': 100
|
||||
'buttonPunch': 100,
|
||||
}.get(name, -1)
|
||||
if devicename in [
|
||||
'Amazon Remote', 'Amazon Bluetooth Dev',
|
||||
'Amazon Fire TV Remote'
|
||||
'Amazon Remote',
|
||||
'Amazon Bluetooth Dev',
|
||||
'Amazon Fire TV Remote',
|
||||
]:
|
||||
return {
|
||||
'triggerRun2': 23,
|
||||
|
|
@ -178,7 +179,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonRight': 23,
|
||||
'buttonStart': 83,
|
||||
'buttonPunch': 90,
|
||||
'buttonDown': 21
|
||||
'buttonDown': 21,
|
||||
}.get(name, -1)
|
||||
|
||||
elif 'NVIDIA SHIELD;' in useragentstring:
|
||||
|
|
@ -193,7 +194,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonStart': 109,
|
||||
'buttonPunch': 100,
|
||||
'buttonIgnored': 184,
|
||||
'buttonIgnored2': 86
|
||||
'buttonIgnored2': 86,
|
||||
}.get(name, -1)
|
||||
elif platform == 'mac':
|
||||
if devicename == 'PLAYSTATION(R)3 Controller':
|
||||
|
|
@ -207,7 +208,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonBomb': 14,
|
||||
'buttonPickUp': 13,
|
||||
'buttonStart': 4,
|
||||
'buttonIgnored': 17
|
||||
'buttonIgnored': 17,
|
||||
}.get(name, -1)
|
||||
if devicename in ['Wireless 360 Controller', 'Controller']:
|
||||
|
||||
|
|
@ -225,16 +226,18 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonUp': 1,
|
||||
'triggerRun1': 5,
|
||||
'triggerRun2': 6,
|
||||
'buttonIgnored': 11
|
||||
'buttonIgnored': 11,
|
||||
}.get(name, -1)
|
||||
if (devicename
|
||||
in ['Logitech Dual Action', 'Logitech Cordless RumblePad 2']):
|
||||
if devicename in [
|
||||
'Logitech Dual Action',
|
||||
'Logitech Cordless RumblePad 2',
|
||||
]:
|
||||
return {
|
||||
'buttonJump': 2,
|
||||
'buttonPunch': 1,
|
||||
'buttonBomb': 3,
|
||||
'buttonPickUp': 4,
|
||||
'buttonStart': 10
|
||||
'buttonStart': 10,
|
||||
}.get(name, -1)
|
||||
|
||||
# Old gravis gamepad.
|
||||
|
|
@ -244,7 +247,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonPunch': 1,
|
||||
'buttonBomb': 3,
|
||||
'buttonPickUp': 4,
|
||||
'buttonStart': 10
|
||||
'buttonStart': 10,
|
||||
}.get(name, -1)
|
||||
|
||||
if devicename == 'Microsoft SideWinder Plug & Play Game Pad':
|
||||
|
|
@ -253,7 +256,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonPunch': 3,
|
||||
'buttonBomb': 2,
|
||||
'buttonPickUp': 4,
|
||||
'buttonStart': 6
|
||||
'buttonStart': 6,
|
||||
}.get(name, -1)
|
||||
|
||||
# Saitek P2500 Rumble Force Pad.. (hopefully works for others too?..)
|
||||
|
|
@ -263,7 +266,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonPunch': 1,
|
||||
'buttonBomb': 4,
|
||||
'buttonPickUp': 2,
|
||||
'buttonStart': 11
|
||||
'buttonStart': 11,
|
||||
}.get(name, -1)
|
||||
|
||||
# Some crazy 'Senze' dual gamepad.
|
||||
|
|
@ -288,7 +291,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonStart': 10,
|
||||
'buttonStart_B': 22,
|
||||
'enableSecondary': 1,
|
||||
'unassignedButtonsRun': False
|
||||
'unassignedButtonsRun': False,
|
||||
}.get(name, -1)
|
||||
if devicename == 'USB Gamepad ': # some weird 'JITE' gamepad
|
||||
return {
|
||||
|
|
@ -298,7 +301,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonPunch': 4,
|
||||
'buttonBomb': 2,
|
||||
'buttonPickUp': 1,
|
||||
'buttonStart': 10
|
||||
'buttonStart': 10,
|
||||
}.get(name, -1)
|
||||
|
||||
default_android_mapping = {
|
||||
|
|
@ -317,7 +320,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonRight': 23,
|
||||
'buttonUp': 20,
|
||||
'buttonDown': 21,
|
||||
'buttonVRReorient': 110
|
||||
'buttonVRReorient': 110,
|
||||
}
|
||||
|
||||
# Generic android...
|
||||
|
|
@ -341,7 +344,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonRight': 23,
|
||||
'buttonUp': 20,
|
||||
'buttonDown': 21,
|
||||
'buttonVRReorient': 108
|
||||
'buttonVRReorient': 108,
|
||||
}.get(name, -1)
|
||||
|
||||
# Adt-1 gamepad (use funky 'mode' button for start).
|
||||
|
|
@ -357,7 +360,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'startButtonActivatesDefaultWidget': False,
|
||||
'buttonRun2': 104,
|
||||
'buttonRun1': 103,
|
||||
'triggerRun1': 18
|
||||
'triggerRun1': 18,
|
||||
}.get(name, -1)
|
||||
# Nexus player remote.
|
||||
if devicename == 'Nexus Remote':
|
||||
|
|
@ -376,7 +379,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonPunch': 24,
|
||||
'buttonRun2': 104,
|
||||
'buttonRun1': 103,
|
||||
'triggerRun1': 18
|
||||
'triggerRun1': 18,
|
||||
}.get(name, -1)
|
||||
|
||||
if devicename == 'virtual-remote':
|
||||
|
|
@ -397,13 +400,15 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonRun1': 103,
|
||||
'buttonDown': 21,
|
||||
'startButtonActivatesDefaultWidget': False,
|
||||
'uiOnly': True
|
||||
'uiOnly': True,
|
||||
}.get(name, -1)
|
||||
|
||||
# flag particular gamepads to use exact android defaults..
|
||||
# (so they don't even ask to configure themselves)
|
||||
if devicename in ['Samsung Game Pad EI-GP20', 'ASUS Gamepad'
|
||||
] or devicename.startswith('Freefly VR Glide'):
|
||||
if devicename in [
|
||||
'Samsung Game Pad EI-GP20',
|
||||
'ASUS Gamepad',
|
||||
] or devicename.startswith('Freefly VR Glide'):
|
||||
return default_android_mapping.get(name, -1)
|
||||
|
||||
# Nvidia controller is default, but gets some strange
|
||||
|
|
@ -423,7 +428,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonPunch': 100,
|
||||
'buttonRun2': 104,
|
||||
'buttonRun1': 103,
|
||||
'triggerRun1': 18
|
||||
'triggerRun1': 18,
|
||||
}.get(name, -1)
|
||||
|
||||
# Default keyboard vals across platforms..
|
||||
|
|
@ -438,7 +443,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonDown': 274,
|
||||
'buttonLeft': 276,
|
||||
'buttonRight': 275,
|
||||
'buttonStart': 263
|
||||
'buttonStart': 263,
|
||||
}.get(name, -1)
|
||||
return {
|
||||
'buttonPickUp': 1073741917,
|
||||
|
|
@ -449,7 +454,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonRight': 1073741903,
|
||||
'buttonStart': 1073741919,
|
||||
'buttonPunch': 1073741913,
|
||||
'buttonDown': 1073741905
|
||||
'buttonDown': 1073741905,
|
||||
}.get(name, -1)
|
||||
if devicename == 'Keyboard' and unique_id == '#1':
|
||||
return {
|
||||
|
|
@ -460,7 +465,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonUp': 119,
|
||||
'buttonDown': 115,
|
||||
'buttonLeft': 97,
|
||||
'buttonRight': 100
|
||||
'buttonRight': 100,
|
||||
}.get(name, -1)
|
||||
|
||||
# Ok, this gamepad's not in our specific preset list;
|
||||
|
|
@ -479,7 +484,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonPunch': 16,
|
||||
'buttonBomb': 14,
|
||||
'buttonPickUp': 13,
|
||||
'buttonStart': 4
|
||||
'buttonStart': 4,
|
||||
}.get(name, -1)
|
||||
|
||||
# Dual Action Config - hopefully applies to more...
|
||||
|
|
@ -489,7 +494,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonPunch': 1,
|
||||
'buttonBomb': 3,
|
||||
'buttonPickUp': 4,
|
||||
'buttonStart': 10
|
||||
'buttonStart': 10,
|
||||
}.get(name, -1)
|
||||
|
||||
# Saitek P2500 Rumble Force Pad.. (hopefully works for others too?..)
|
||||
|
|
@ -499,7 +504,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonPunch': 1,
|
||||
'buttonBomb': 4,
|
||||
'buttonPickUp': 2,
|
||||
'buttonStart': 11
|
||||
'buttonStart': 11,
|
||||
}.get(name, -1)
|
||||
|
||||
# Gravis stuff?...
|
||||
|
|
@ -509,7 +514,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonPunch': 1,
|
||||
'buttonBomb': 3,
|
||||
'buttonPickUp': 4,
|
||||
'buttonStart': 10
|
||||
'buttonStart': 10,
|
||||
}.get(name, -1)
|
||||
|
||||
# Reasonable defaults.
|
||||
|
|
@ -542,21 +547,23 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonPunch': 2,
|
||||
'buttonBomb': 3,
|
||||
'buttonPickUp': 4,
|
||||
'buttonStart': 5
|
||||
'buttonStart': 5,
|
||||
}.get(name, -1)
|
||||
|
||||
|
||||
def _gen_android_input_hash() -> str:
|
||||
import os
|
||||
import hashlib
|
||||
|
||||
md5 = hashlib.md5()
|
||||
|
||||
# Currently we just do a single hash of *all* inputs on android
|
||||
# and that's it.. good enough.
|
||||
# (grabbing mappings for a specific device looks to be non-trivial)
|
||||
for dirname in [
|
||||
'/system/usr/keylayout', '/data/usr/keylayout',
|
||||
'/data/system/devices/keylayout'
|
||||
'/system/usr/keylayout',
|
||||
'/data/usr/keylayout',
|
||||
'/data/system/devices/keylayout',
|
||||
]:
|
||||
try:
|
||||
if os.path.isdir(dirname):
|
||||
|
|
@ -573,8 +580,10 @@ def _gen_android_input_hash() -> str:
|
|||
pass
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception(
|
||||
'error in _gen_android_input_hash inner loop')
|
||||
'error in _gen_android_input_hash inner loop'
|
||||
)
|
||||
return md5.hexdigest()
|
||||
|
||||
|
||||
|
|
@ -597,12 +606,14 @@ def get_input_map_hash(inputdevice: ba.InputDevice) -> str:
|
|||
return app.input_map_hash
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception('Exception in get_input_map_hash')
|
||||
return ''
|
||||
|
||||
|
||||
def get_input_device_config(device: ba.InputDevice,
|
||||
default: bool) -> tuple[dict, str]:
|
||||
def get_input_device_config(
|
||||
device: ba.InputDevice, default: bool
|
||||
) -> tuple[dict, str]:
|
||||
"""Given an input device, return its config dict in the app config.
|
||||
|
||||
The dict will be created if it does not exist.
|
||||
|
|
@ -634,8 +645,10 @@ def get_last_player_name_from_input_device(device: ba.InputDevice) -> str:
|
|||
# otherwise default to their current random name.
|
||||
profilename = '_random'
|
||||
key_name = device.name + ' ' + device.unique_identifier
|
||||
if ('Default Player Profiles' in appconfig
|
||||
and key_name in appconfig['Default Player Profiles']):
|
||||
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()
|
||||
|
|
|
|||
86
dist/ba_data/python/ba/_internal.py
vendored
86
dist/ba_data/python/ba/_internal.py
vendored
|
|
@ -14,6 +14,7 @@ from typing import TYPE_CHECKING
|
|||
try:
|
||||
# noinspection PyUnresolvedReferences
|
||||
import _bainternal
|
||||
|
||||
HAVE_INTERNAL = True
|
||||
except ImportError:
|
||||
HAVE_INTERNAL = False
|
||||
|
|
@ -26,6 +27,7 @@ if TYPE_CHECKING:
|
|||
# to account for its absence should call this to draw attention to itself.
|
||||
def _no_bainternal_warning() -> None:
|
||||
import logging
|
||||
|
||||
logging.warning('INTERNAL CALL RUN WITHOUT INTERNAL PRESENT.')
|
||||
|
||||
|
||||
|
|
@ -47,8 +49,9 @@ def get_master_server_address(source: int = -1, version: int = 1) -> str:
|
|||
Return the address of the master server.
|
||||
"""
|
||||
if HAVE_INTERNAL:
|
||||
return _bainternal.get_master_server_address(source=source,
|
||||
version=version)
|
||||
return _bainternal.get_master_server_address(
|
||||
source=source, version=version
|
||||
)
|
||||
raise _no_bainternal_error()
|
||||
|
||||
|
||||
|
|
@ -75,8 +78,9 @@ def game_service_has_leaderboard(game: str, config: str) -> bool:
|
|||
for it on the game service.
|
||||
"""
|
||||
if HAVE_INTERNAL:
|
||||
return _bainternal.game_service_has_leaderboard(game=game,
|
||||
config=config)
|
||||
return _bainternal.game_service_has_leaderboard(
|
||||
game=game, config=config
|
||||
)
|
||||
# Harmless to always just say no here.
|
||||
return False
|
||||
|
||||
|
|
@ -84,8 +88,9 @@ def game_service_has_leaderboard(game: str, config: str) -> bool:
|
|||
def report_achievement(achievement: str, pass_to_account: bool = True) -> None:
|
||||
"""(internal)"""
|
||||
if HAVE_INTERNAL:
|
||||
_bainternal.report_achievement(achievement=achievement,
|
||||
pass_to_account=pass_to_account)
|
||||
_bainternal.report_achievement(
|
||||
achievement=achievement, pass_to_account=pass_to_account
|
||||
)
|
||||
return
|
||||
|
||||
# Need to see if this actually still works as expected.. warning for now.
|
||||
|
|
@ -93,17 +98,19 @@ def report_achievement(achievement: str, pass_to_account: bool = True) -> None:
|
|||
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
def submit_score(game: str,
|
||||
config: str,
|
||||
name: Any,
|
||||
score: int | None,
|
||||
callback: Callable,
|
||||
friend_callback: Callable | None,
|
||||
order: str = 'increasing',
|
||||
tournament_id: str | None = None,
|
||||
score_type: str = 'points',
|
||||
campaign: str | None = None,
|
||||
level: str | None = None) -> None:
|
||||
def submit_score(
|
||||
game: str,
|
||||
config: str,
|
||||
name: Any,
|
||||
score: int | None,
|
||||
callback: Callable,
|
||||
friend_callback: Callable | None,
|
||||
order: str = 'increasing',
|
||||
tournament_id: str | None = None,
|
||||
score_type: str = 'points',
|
||||
campaign: str | None = None,
|
||||
level: str | None = None,
|
||||
) -> None:
|
||||
"""(internal)
|
||||
|
||||
Submit a score to the server; callback will be called with the results.
|
||||
|
|
@ -112,24 +119,27 @@ def submit_score(game: str,
|
|||
score server more mischief-proof.
|
||||
"""
|
||||
if HAVE_INTERNAL:
|
||||
_bainternal.submit_score(game=game,
|
||||
config=config,
|
||||
name=name,
|
||||
score=score,
|
||||
callback=callback,
|
||||
friend_callback=friend_callback,
|
||||
order=order,
|
||||
tournament_id=tournament_id,
|
||||
score_type=score_type,
|
||||
campaign=campaign,
|
||||
level=level)
|
||||
_bainternal.submit_score(
|
||||
game=game,
|
||||
config=config,
|
||||
name=name,
|
||||
score=score,
|
||||
callback=callback,
|
||||
friend_callback=friend_callback,
|
||||
order=order,
|
||||
tournament_id=tournament_id,
|
||||
score_type=score_type,
|
||||
campaign=campaign,
|
||||
level=level,
|
||||
)
|
||||
return
|
||||
# This technically breaks since callback will never be called/etc.
|
||||
raise _no_bainternal_error()
|
||||
|
||||
|
||||
def tournament_query(callback: Callable[[dict | None], None],
|
||||
args: dict) -> None:
|
||||
def tournament_query(
|
||||
callback: Callable[[dict | None], None], args: dict
|
||||
) -> None:
|
||||
"""(internal)"""
|
||||
if HAVE_INTERNAL:
|
||||
_bainternal.tournament_query(callback=callback, args=args)
|
||||
|
|
@ -212,8 +222,9 @@ def in_game_purchase(item: str, price: int) -> None:
|
|||
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
def add_transaction(transaction: dict,
|
||||
callback: Callable | None = None) -> None:
|
||||
def add_transaction(
|
||||
transaction: dict, callback: Callable | None = None
|
||||
) -> None:
|
||||
"""(internal)"""
|
||||
if HAVE_INTERNAL:
|
||||
_bainternal.add_transaction(transaction=transaction, callback=callback)
|
||||
|
|
@ -265,7 +276,8 @@ def get_v1_account_misc_read_val(name: str, default_value: Any) -> Any:
|
|||
"""(internal)"""
|
||||
if HAVE_INTERNAL:
|
||||
return _bainternal.get_v1_account_misc_read_val(
|
||||
name=name, default_value=default_value)
|
||||
name=name, default_value=default_value
|
||||
)
|
||||
raise _no_bainternal_error()
|
||||
|
||||
|
||||
|
|
@ -273,15 +285,17 @@ def get_v1_account_misc_read_val_2(name: str, default_value: Any) -> Any:
|
|||
"""(internal)"""
|
||||
if HAVE_INTERNAL:
|
||||
return _bainternal.get_v1_account_misc_read_val_2(
|
||||
name=name, default_value=default_value)
|
||||
name=name, default_value=default_value
|
||||
)
|
||||
raise _no_bainternal_error()
|
||||
|
||||
|
||||
def get_v1_account_misc_val(name: str, default_value: Any) -> Any:
|
||||
"""(internal)"""
|
||||
if HAVE_INTERNAL:
|
||||
return _bainternal.get_v1_account_misc_val(name=name,
|
||||
default_value=default_value)
|
||||
return _bainternal.get_v1_account_misc_val(
|
||||
name=name, default_value=default_value
|
||||
)
|
||||
raise _no_bainternal_error()
|
||||
|
||||
|
||||
|
|
|
|||
198
dist/ba_data/python/ba/_language.py
vendored
198
dist/ba_data/python/ba/_language.py
vendored
|
|
@ -35,10 +35,21 @@ class LanguageSubsystem:
|
|||
"""
|
||||
|
||||
# We don't yet support full unicode display on windows or linux :-(.
|
||||
if (language in {
|
||||
'Chinese', 'ChineseTraditional', 'Persian', 'Korean', 'Arabic',
|
||||
'Hindi', 'Vietnamese', 'Thai', 'Tamil'
|
||||
} and not _ba.can_display_full_unicode()):
|
||||
if (
|
||||
language
|
||||
in {
|
||||
'Chinese',
|
||||
'ChineseTraditional',
|
||||
'Persian',
|
||||
'Korean',
|
||||
'Arabic',
|
||||
'Hindi',
|
||||
'Vietnamese',
|
||||
'Thai',
|
||||
'Tamil',
|
||||
}
|
||||
and not _ba.can_display_full_unicode()
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
|
@ -130,18 +141,22 @@ class LanguageSubsystem:
|
|||
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))
|
||||
return sorted(
|
||||
name for name in names if self._can_display_language(name)
|
||||
)
|
||||
|
||||
def setlanguage(self,
|
||||
language: str | None,
|
||||
print_change: bool = True,
|
||||
store_to_config: bool = True) -> None:
|
||||
def setlanguage(
|
||||
self,
|
||||
language: str | None,
|
||||
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.
|
||||
|
|
@ -164,8 +179,9 @@ class LanguageSubsystem:
|
|||
else:
|
||||
switched = False
|
||||
|
||||
with open('ba_data/data/languages/english.json',
|
||||
encoding='utf-8') as infile:
|
||||
with open(
|
||||
'ba_data/data/languages/english.json', encoding='utf-8'
|
||||
) as infile:
|
||||
lenglishvalues = json.loads(infile.read())
|
||||
|
||||
# None implies default.
|
||||
|
|
@ -175,16 +191,21 @@ class LanguageSubsystem:
|
|||
if language == 'English':
|
||||
lmodvalues = None
|
||||
else:
|
||||
lmodfile = 'ba_data/data/languages/' + language.lower(
|
||||
) + '.json'
|
||||
lmodfile = (
|
||||
'ba_data/data/languages/' + language.lower() + '.json'
|
||||
)
|
||||
with open(lmodfile, encoding='utf-8') as infile:
|
||||
lmodvalues = json.loads(infile.read())
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception('Exception importing language:', language)
|
||||
_ba.screenmessage("Error setting language to '" + language +
|
||||
"'; see log for details",
|
||||
color=(1, 0, 0))
|
||||
_ba.screenmessage(
|
||||
"Error setting language to '"
|
||||
+ language
|
||||
+ "'; see log for details",
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
switched = False
|
||||
lmodvalues = None
|
||||
|
||||
|
|
@ -193,8 +214,8 @@ class LanguageSubsystem:
|
|||
langtarget = self.language_target
|
||||
assert langtarget is not None
|
||||
_add_to_attr_dict(
|
||||
langtarget,
|
||||
lmodvalues if lmodvalues is not None else lenglishvalues)
|
||||
langtarget, lmodvalues if lmodvalues is not None else lenglishvalues
|
||||
)
|
||||
|
||||
# Create an attrdict of our target language overlaid
|
||||
# on our base (english).
|
||||
|
|
@ -209,20 +230,22 @@ class LanguageSubsystem:
|
|||
# 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)
|
||||
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'
|
||||
'replayNameDefaultText',
|
||||
'replayWriteErrorText',
|
||||
'replayVersionErrorText',
|
||||
'replayReadErrorText',
|
||||
]:
|
||||
internal_vals.append((value, lfull[value]))
|
||||
internal_vals.append(
|
||||
('axisText', lfull['configGamepadWindow']['axisText']))
|
||||
('axisText', lfull['configGamepadWindow']['axisText'])
|
||||
)
|
||||
internal_vals.append(('buttonText', lfull['buttonText']))
|
||||
lmerged = self.language_merged
|
||||
assert lmerged is not None
|
||||
|
|
@ -232,16 +255,22 @@ class LanguageSubsystem:
|
|||
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))
|
||||
_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 = None,
|
||||
fallback_value: Any = None) -> Any:
|
||||
def get_resource(
|
||||
self,
|
||||
resource: str,
|
||||
fallback_resource: str | None = None,
|
||||
fallback_value: Any = None,
|
||||
) -> Any:
|
||||
"""Return a translation resource by name.
|
||||
|
||||
DEPRECATED; use ba.Lstr functionality for these purposes.
|
||||
|
|
@ -251,24 +280,29 @@ class LanguageSubsystem:
|
|||
if self.language_merged is None:
|
||||
language = self.language
|
||||
try:
|
||||
self.setlanguage(language,
|
||||
print_change=False,
|
||||
store_to_config=False)
|
||||
self.setlanguage(
|
||||
language, print_change=False, store_to_config=False
|
||||
)
|
||||
except Exception:
|
||||
from ba import _error
|
||||
_error.print_exception('exception setting language to',
|
||||
language)
|
||||
|
||||
_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)
|
||||
self.setlanguage(
|
||||
'English',
|
||||
print_change=False,
|
||||
store_to_config=False,
|
||||
)
|
||||
except Exception:
|
||||
_error.print_exception(
|
||||
'error setting language to english fallback')
|
||||
'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
|
||||
|
|
@ -325,16 +359,20 @@ class LanguageSubsystem:
|
|||
# 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
|
||||
f"Resource not found: '{resource}'"
|
||||
) from None
|
||||
|
||||
def translate(self,
|
||||
category: str,
|
||||
strval: str,
|
||||
raise_exceptions: bool = False,
|
||||
print_errors: bool = False) -> str:
|
||||
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.
|
||||
|
|
@ -345,8 +383,17 @@ class LanguageSubsystem:
|
|||
if raise_exceptions:
|
||||
raise
|
||||
if print_errors:
|
||||
print(('Translate error: category=\'' + category +
|
||||
'\' name=\'' + strval + '\' exc=' + str(exc) + ''))
|
||||
print(
|
||||
(
|
||||
'Translate error: category=\''
|
||||
+ category
|
||||
+ '\' name=\''
|
||||
+ strval
|
||||
+ '\' exc='
|
||||
+ str(exc)
|
||||
+ ''
|
||||
)
|
||||
)
|
||||
translated = None
|
||||
translated_out: str
|
||||
if translated is None:
|
||||
|
|
@ -403,28 +450,31 @@ class Lstr:
|
|||
# pylint: disable=dangerous-default-value
|
||||
# noinspection PyDefaultArgument
|
||||
@overload
|
||||
def __init__(self,
|
||||
*,
|
||||
resource: str,
|
||||
fallback_resource: str = '',
|
||||
fallback_value: str = '',
|
||||
subs: Sequence[tuple[str, str | Lstr]] = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
resource: str,
|
||||
fallback_resource: str = '',
|
||||
fallback_value: str = '',
|
||||
subs: Sequence[tuple[str, 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, str | Lstr]] = []) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
translate: tuple[str, str],
|
||||
subs: Sequence[tuple[str, 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, str | Lstr]] = []) -> None:
|
||||
def __init__(
|
||||
self, *, value: str, subs: Sequence[tuple[str, str | Lstr]] = []
|
||||
) -> None:
|
||||
"""Create an Lstr from a raw string value."""
|
||||
|
||||
# pylint: enable=redefined-outer-name, dangerous-default-value
|
||||
|
|
@ -478,10 +528,12 @@ class Lstr:
|
|||
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)
|
||||
once=True,
|
||||
)
|
||||
keywds['f'] = keywds['fallback']
|
||||
del keywds['fallback']
|
||||
if 'fallback_resource' in keywds:
|
||||
|
|
@ -517,6 +569,7 @@ class Lstr:
|
|||
return json.dumps(self.args, separators=(',', ':'))
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception('_get_json failed for', self.args)
|
||||
return 'JSON_ERR'
|
||||
|
||||
|
|
@ -542,13 +595,20 @@ def _add_to_attr_dict(dst: AttrDict, src: dict) -> None:
|
|||
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")
|
||||
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)))
|
||||
raise TypeError(
|
||||
"invalid value type for res '"
|
||||
+ key
|
||||
+ "': "
|
||||
+ str(type(value))
|
||||
)
|
||||
dst[key] = value
|
||||
|
||||
|
||||
|
|
|
|||
36
dist/ba_data/python/ba/_level.py
vendored
36
dist/ba_data/python/ba/_level.py
vendored
|
|
@ -20,12 +20,14 @@ class Level:
|
|||
Category: **Gameplay Classes**
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
name: str,
|
||||
gametype: type[ba.GameActivity],
|
||||
settings: dict,
|
||||
preview_texture_name: str,
|
||||
displayname: str | None = None):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
gametype: type[ba.GameActivity],
|
||||
settings: dict,
|
||||
preview_texture_name: str,
|
||||
displayname: str | None = None,
|
||||
):
|
||||
self._name = name
|
||||
self._gametype = gametype
|
||||
self._settings = settings
|
||||
|
|
@ -66,11 +68,18 @@ class Level:
|
|||
def displayname(self) -> ba.Lstr:
|
||||
"""The localized name for this Level."""
|
||||
from ba import _language
|
||||
|
||||
return _language.Lstr(
|
||||
translate=('coopLevelNames', self._displayname
|
||||
if self._displayname is not None else self._name),
|
||||
subs=[('${GAME}',
|
||||
self._gametype.get_display_string(self._settings))])
|
||||
translate=(
|
||||
'coopLevelNames',
|
||||
self._displayname
|
||||
if self._displayname is not None
|
||||
else self._name,
|
||||
),
|
||||
subs=[
|
||||
('${GAME}', self._gametype.get_display_string(self._settings))
|
||||
],
|
||||
)
|
||||
|
||||
@property
|
||||
def gametype(self) -> type[ba.GameActivity]:
|
||||
|
|
@ -156,10 +165,9 @@ class Level:
|
|||
if campaign is None:
|
||||
raise RuntimeError('Level is not in a campaign.')
|
||||
configdict = campaign.configdict
|
||||
val: dict[str, Any] = configdict.setdefault(self._name, {
|
||||
'Rating': 0.0,
|
||||
'Complete': False
|
||||
})
|
||||
val: dict[str, Any] = configdict.setdefault(
|
||||
self._name, {'Rating': 0.0, 'Complete': False}
|
||||
)
|
||||
assert isinstance(val, dict)
|
||||
return val
|
||||
|
||||
|
|
|
|||
477
dist/ba_data/python/ba/_lobby.py
vendored
477
dist/ba_data/python/ba/_lobby.py
vendored
|
|
@ -1,6 +1,7 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Implements lobby system for gathering before games, char select, etc."""
|
||||
# pylint: disable=too-many-lines
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -31,15 +32,20 @@ class JoinInfo:
|
|||
def __init__(self, lobby: ba.Lobby):
|
||||
from ba._nodeactor import NodeActor
|
||||
from ba._general import WeakCall
|
||||
|
||||
self._state = 0
|
||||
self._press_to_punch: str | ba.Lstr = ('C' if _ba.app.iircade_mode else
|
||||
_ba.charstr(
|
||||
SpecialChar.LEFT_BUTTON))
|
||||
self._press_to_bomb: str | ba.Lstr = ('B' if _ba.app.iircade_mode else
|
||||
_ba.charstr(
|
||||
SpecialChar.RIGHT_BUTTON))
|
||||
self._press_to_punch: str | ba.Lstr = (
|
||||
'C'
|
||||
if _ba.app.iircade_mode
|
||||
else _ba.charstr(SpecialChar.LEFT_BUTTON)
|
||||
)
|
||||
self._press_to_bomb: str | ba.Lstr = (
|
||||
'B'
|
||||
if _ba.app.iircade_mode
|
||||
else _ba.charstr(SpecialChar.RIGHT_BUTTON)
|
||||
)
|
||||
self._joinmsg = Lstr(resource='pressAnyButtonToJoinText')
|
||||
can_switch_teams = (len(lobby.sessionteams) > 1)
|
||||
can_switch_teams = len(lobby.sessionteams) > 1
|
||||
|
||||
# If we have a keyboard, grab keys for punch and pickup.
|
||||
# FIXME: This of course is only correct on the local device;
|
||||
|
|
@ -50,59 +56,97 @@ class JoinInfo:
|
|||
|
||||
flatness = 1.0 if _ba.app.vr_mode else 0.0
|
||||
self._text = NodeActor(
|
||||
_ba.newnode('text',
|
||||
attrs={
|
||||
'position': (0, -40),
|
||||
'h_attach': 'center',
|
||||
'v_attach': 'top',
|
||||
'h_align': 'center',
|
||||
'color': (0.7, 0.7, 0.95, 1.0),
|
||||
'flatness': flatness,
|
||||
'text': self._joinmsg
|
||||
}))
|
||||
_ba.newnode(
|
||||
'text',
|
||||
attrs={
|
||||
'position': (0, -40),
|
||||
'h_attach': 'center',
|
||||
'v_attach': 'top',
|
||||
'h_align': 'center',
|
||||
'color': (0.7, 0.7, 0.95, 1.0),
|
||||
'flatness': flatness,
|
||||
'text': self._joinmsg,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
if _ba.app.demo_mode or _ba.app.arcade_mode:
|
||||
self._messages = [self._joinmsg]
|
||||
else:
|
||||
msg1 = Lstr(resource='pressToSelectProfileText',
|
||||
subs=[
|
||||
('${BUTTONS}', _ba.charstr(SpecialChar.UP_ARROW) +
|
||||
' ' + _ba.charstr(SpecialChar.DOWN_ARROW))
|
||||
])
|
||||
msg2 = Lstr(resource='pressToOverrideCharacterText',
|
||||
subs=[('${BUTTONS}', Lstr(resource='bombBoldText'))])
|
||||
msg3 = Lstr(value='${A} < ${B} >',
|
||||
subs=[('${A}', msg2), ('${B}', self._press_to_bomb)])
|
||||
self._messages = (([
|
||||
Lstr(
|
||||
resource='pressToSelectTeamText',
|
||||
subs=[('${BUTTONS}', _ba.charstr(SpecialChar.LEFT_ARROW) +
|
||||
' ' + _ba.charstr(SpecialChar.RIGHT_ARROW))],
|
||||
msg1 = Lstr(
|
||||
resource='pressToSelectProfileText',
|
||||
subs=[
|
||||
(
|
||||
'${BUTTONS}',
|
||||
_ba.charstr(SpecialChar.UP_ARROW)
|
||||
+ ' '
|
||||
+ _ba.charstr(SpecialChar.DOWN_ARROW),
|
||||
)
|
||||
],
|
||||
)
|
||||
msg2 = Lstr(
|
||||
resource='pressToOverrideCharacterText',
|
||||
subs=[('${BUTTONS}', Lstr(resource='bombBoldText'))],
|
||||
)
|
||||
msg3 = Lstr(
|
||||
value='${A} < ${B} >',
|
||||
subs=[('${A}', msg2), ('${B}', self._press_to_bomb)],
|
||||
)
|
||||
self._messages = (
|
||||
(
|
||||
[
|
||||
Lstr(
|
||||
resource='pressToSelectTeamText',
|
||||
subs=[
|
||||
(
|
||||
'${BUTTONS}',
|
||||
_ba.charstr(SpecialChar.LEFT_ARROW)
|
||||
+ ' '
|
||||
+ _ba.charstr(SpecialChar.RIGHT_ARROW),
|
||||
)
|
||||
],
|
||||
)
|
||||
]
|
||||
if can_switch_teams
|
||||
else []
|
||||
)
|
||||
] if can_switch_teams else []) + [msg1] + [msg3] + [self._joinmsg])
|
||||
+ [msg1]
|
||||
+ [msg3]
|
||||
+ [self._joinmsg]
|
||||
)
|
||||
|
||||
self._timer = _ba.Timer(4.0, WeakCall(self._update), repeat=True)
|
||||
|
||||
def _update_for_keyboard(self, keyboard: ba.InputDevice) -> None:
|
||||
from ba import _input
|
||||
|
||||
punch_key = keyboard.get_button_name(
|
||||
_input.get_device_value(keyboard, 'buttonPunch'))
|
||||
self._press_to_punch = Lstr(resource='orText',
|
||||
subs=[('${A}',
|
||||
Lstr(value='\'${K}\'',
|
||||
subs=[('${K}', punch_key)])),
|
||||
('${B}', self._press_to_punch)])
|
||||
_input.get_device_value(keyboard, 'buttonPunch')
|
||||
)
|
||||
self._press_to_punch = Lstr(
|
||||
resource='orText',
|
||||
subs=[
|
||||
('${A}', Lstr(value='\'${K}\'', subs=[('${K}', punch_key)])),
|
||||
('${B}', self._press_to_punch),
|
||||
],
|
||||
)
|
||||
bomb_key = keyboard.get_button_name(
|
||||
_input.get_device_value(keyboard, 'buttonBomb'))
|
||||
self._press_to_bomb = Lstr(resource='orText',
|
||||
subs=[('${A}',
|
||||
Lstr(value='\'${K}\'',
|
||||
subs=[('${K}', bomb_key)])),
|
||||
('${B}', self._press_to_bomb)])
|
||||
self._joinmsg = Lstr(value='${A} < ${B} >',
|
||||
subs=[('${A}',
|
||||
Lstr(resource='pressPunchToJoinText')),
|
||||
('${B}', self._press_to_punch)])
|
||||
_input.get_device_value(keyboard, 'buttonBomb')
|
||||
)
|
||||
self._press_to_bomb = Lstr(
|
||||
resource='orText',
|
||||
subs=[
|
||||
('${A}', Lstr(value='\'${K}\'', subs=[('${K}', bomb_key)])),
|
||||
('${B}', self._press_to_bomb),
|
||||
],
|
||||
)
|
||||
self._joinmsg = Lstr(
|
||||
value='${A} < ${B} >',
|
||||
subs=[
|
||||
('${A}', Lstr(resource='pressPunchToJoinText')),
|
||||
('${B}', self._press_to_punch),
|
||||
],
|
||||
)
|
||||
|
||||
def _update(self) -> None:
|
||||
assert self._text.node
|
||||
|
|
@ -113,12 +157,14 @@ class JoinInfo:
|
|||
@dataclass
|
||||
class PlayerReadyMessage:
|
||||
"""Tells an object a player has been selected from the given chooser."""
|
||||
|
||||
chooser: ba.Chooser
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChangeMessage:
|
||||
"""Tells an object that a selection is being changed."""
|
||||
|
||||
what: str
|
||||
value: int
|
||||
|
||||
|
|
@ -135,8 +181,9 @@ class Chooser:
|
|||
if self._text_node:
|
||||
self._text_node.delete()
|
||||
|
||||
def __init__(self, vpos: float, sessionplayer: _ba.SessionPlayer,
|
||||
lobby: 'Lobby') -> None:
|
||||
def __init__(
|
||||
self, vpos: float, sessionplayer: _ba.SessionPlayer, lobby: 'Lobby'
|
||||
) -> None:
|
||||
self._deek_sound = _ba.getsound('deek')
|
||||
self._click_sound = _ba.getsound('click01')
|
||||
self._punchsound = _ba.getsound('punch01')
|
||||
|
|
@ -170,48 +217,54 @@ class Chooser:
|
|||
# for the '_random' profile. Let's use their input_device id to seed
|
||||
# it. This will give a persistent character for them between games
|
||||
# and will distribute characters nicely if everyone is random.
|
||||
self._random_color, self._random_highlight = (
|
||||
get_player_profile_colors(None))
|
||||
self._random_color, self._random_highlight = get_player_profile_colors(
|
||||
None
|
||||
)
|
||||
|
||||
# To calc our random character we pick a random one out of our
|
||||
# unlocked list and then locate that character's index in the full
|
||||
# list.
|
||||
char_index_offset = app.lobby_random_char_index_offset
|
||||
self._random_character_index = (
|
||||
(sessionplayer.inputdevice.id + char_index_offset) %
|
||||
len(self._character_names))
|
||||
sessionplayer.inputdevice.id + char_index_offset
|
||||
) % len(self._character_names)
|
||||
|
||||
# Attempt to set an initial profile based on what was used previously
|
||||
# for this input-device, etc.
|
||||
self._profileindex = self._select_initial_profile()
|
||||
self._profilename = self._profilenames[self._profileindex]
|
||||
|
||||
self._text_node = _ba.newnode('text',
|
||||
delegate=self,
|
||||
attrs={
|
||||
'position': (-100, self._vpos),
|
||||
'maxwidth': 160,
|
||||
'shadow': 0.5,
|
||||
'vr_depth': -20,
|
||||
'h_align': 'left',
|
||||
'v_align': 'center',
|
||||
'v_attach': 'top'
|
||||
})
|
||||
self._text_node = _ba.newnode(
|
||||
'text',
|
||||
delegate=self,
|
||||
attrs={
|
||||
'position': (-100, self._vpos),
|
||||
'maxwidth': 160,
|
||||
'shadow': 0.5,
|
||||
'vr_depth': -20,
|
||||
'h_align': 'left',
|
||||
'v_align': 'center',
|
||||
'v_attach': 'top',
|
||||
},
|
||||
)
|
||||
animate(self._text_node, 'scale', {0: 0, 0.1: 1.0})
|
||||
self.icon = _ba.newnode('image',
|
||||
owner=self._text_node,
|
||||
attrs={
|
||||
'position': (-130, self._vpos + 20),
|
||||
'mask_texture': self._mask_texture,
|
||||
'vr_depth': -10,
|
||||
'attach': 'topCenter'
|
||||
})
|
||||
self.icon = _ba.newnode(
|
||||
'image',
|
||||
owner=self._text_node,
|
||||
attrs={
|
||||
'position': (-130, self._vpos + 20),
|
||||
'mask_texture': self._mask_texture,
|
||||
'vr_depth': -10,
|
||||
'attach': 'topCenter',
|
||||
},
|
||||
)
|
||||
|
||||
animate_array(self.icon, 'scale', 2, {0: (0, 0), 0.1: (45, 45)})
|
||||
|
||||
# Set our initial name to '<choosing player>' in case anyone asks.
|
||||
self._sessionplayer.setname(
|
||||
Lstr(resource='choosingPlayerText').evaluate(), real=False)
|
||||
Lstr(resource='choosingPlayerText').evaluate(), real=False
|
||||
)
|
||||
|
||||
# Init these to our rando but they should get switched to the
|
||||
# selected profile (if any) right after.
|
||||
|
|
@ -232,32 +285,40 @@ class Chooser:
|
|||
|
||||
# If we've got a set profile name for this device, work backwards
|
||||
# from that to get our index.
|
||||
dprofilename = (app.config.get('Default Player Profiles',
|
||||
{}).get(inputdevice.name + ' ' +
|
||||
inputdevice.unique_identifier))
|
||||
dprofilename = app.config.get('Default Player Profiles', {}).get(
|
||||
inputdevice.name + ' ' + inputdevice.unique_identifier
|
||||
)
|
||||
if dprofilename is not None and dprofilename in profilenames:
|
||||
# If we got '__account__' and its local and we haven't marked
|
||||
# anyone as the 'account profile' device yet, mark this guy as
|
||||
# it. (prevents the next joiner from getting the account
|
||||
# profile too).
|
||||
if (dprofilename == '__account__'
|
||||
and not inputdevice.is_remote_client
|
||||
and app.lobby_account_profile_device_id is None):
|
||||
if (
|
||||
dprofilename == '__account__'
|
||||
and not inputdevice.is_remote_client
|
||||
and app.lobby_account_profile_device_id is None
|
||||
):
|
||||
app.lobby_account_profile_device_id = inputdevice.id
|
||||
return profilenames.index(dprofilename)
|
||||
|
||||
# We want to mark the first local input-device in the game
|
||||
# as the 'account profile' device.
|
||||
if (not inputdevice.is_remote_client
|
||||
and not inputdevice.is_controller_app):
|
||||
if (app.lobby_account_profile_device_id is None
|
||||
and '__account__' in profilenames):
|
||||
if (
|
||||
not inputdevice.is_remote_client
|
||||
and not inputdevice.is_controller_app
|
||||
):
|
||||
if (
|
||||
app.lobby_account_profile_device_id is None
|
||||
and '__account__' in profilenames
|
||||
):
|
||||
app.lobby_account_profile_device_id = inputdevice.id
|
||||
|
||||
# If this is the designated account-profile-device, try to default
|
||||
# to the account profile.
|
||||
if (inputdevice.id == app.lobby_account_profile_device_id
|
||||
and '__account__' in profilenames):
|
||||
if (
|
||||
inputdevice.id == app.lobby_account_profile_device_id
|
||||
and '__account__' in profilenames
|
||||
):
|
||||
return profilenames.index('__account__')
|
||||
|
||||
# If this is the controller app, it defaults to using a random
|
||||
|
|
@ -274,9 +335,13 @@ class Chooser:
|
|||
|
||||
# Cycle through our non-random profiles once; after
|
||||
# that, everyone gets random.
|
||||
while (app.lobby_random_profile_index < len(profilenames)
|
||||
and profilenames[app.lobby_random_profile_index]
|
||||
in ('_random', '__account__', '_edit')):
|
||||
while app.lobby_random_profile_index < len(
|
||||
profilenames
|
||||
) and profilenames[app.lobby_random_profile_index] in (
|
||||
'_random',
|
||||
'__account__',
|
||||
'_edit',
|
||||
):
|
||||
app.lobby_random_profile_index += 1
|
||||
if app.lobby_random_profile_index < len(profilenames):
|
||||
profileindex = app.lobby_random_profile_index
|
||||
|
|
@ -340,18 +405,22 @@ class Chooser:
|
|||
# character to others they own, but profile characters
|
||||
# should work (and we validate profiles on the master server
|
||||
# so no exploit opportunities)
|
||||
if (character not in self._character_names
|
||||
and character in _ba.app.spaz_appearances):
|
||||
if (
|
||||
character not in self._character_names
|
||||
and character in _ba.app.spaz_appearances
|
||||
):
|
||||
self._character_names.append(character)
|
||||
self._character_index = self._character_names.index(character)
|
||||
self._color, self._highlight = (get_player_profile_colors(
|
||||
self._profilename, profiles=self._profiles))
|
||||
self._color, self._highlight = get_player_profile_colors(
|
||||
self._profilename, profiles=self._profiles
|
||||
)
|
||||
self._update_icon()
|
||||
self._update_text()
|
||||
|
||||
def reload_profiles(self) -> None:
|
||||
"""Reload all player profiles."""
|
||||
from ba._general import json_prep
|
||||
|
||||
app = _ba.app
|
||||
|
||||
# Re-construct our profile index and other stuff since the profile
|
||||
|
|
@ -396,8 +465,11 @@ class Chooser:
|
|||
|
||||
# For local devices, add it an 'edit' option which will pop up
|
||||
# the profile window.
|
||||
if not is_remote and not is_test_input and not (app.demo_mode
|
||||
or app.arcade_mode):
|
||||
if (
|
||||
not is_remote
|
||||
and not is_test_input
|
||||
and not (app.demo_mode or app.arcade_mode)
|
||||
):
|
||||
self._profiles['_edit'] = {}
|
||||
|
||||
# Build a sorted name list we can iterate through.
|
||||
|
|
@ -417,18 +489,25 @@ class Chooser:
|
|||
assert self._text_node
|
||||
spacing = 350
|
||||
sessionteams = self.lobby.sessionteams
|
||||
offs = (spacing * -0.5 * len(sessionteams) +
|
||||
spacing * self._selected_team_index + 250)
|
||||
offs = (
|
||||
spacing * -0.5 * len(sessionteams)
|
||||
+ spacing * self._selected_team_index
|
||||
+ 250
|
||||
)
|
||||
if len(sessionteams) > 1:
|
||||
offs -= 35
|
||||
animate_array(self._text_node, 'position', 2, {
|
||||
0: self._text_node.position,
|
||||
0.1: (-100 + offs, self._vpos + 23)
|
||||
})
|
||||
animate_array(self.icon, 'position', 2, {
|
||||
0: self.icon.position,
|
||||
0.1: (-130 + offs, self._vpos + 22)
|
||||
})
|
||||
animate_array(
|
||||
self._text_node,
|
||||
'position',
|
||||
2,
|
||||
{0: self._text_node.position, 0.1: (-100 + offs, self._vpos + 23)},
|
||||
)
|
||||
animate_array(
|
||||
self.icon,
|
||||
'position',
|
||||
2,
|
||||
{0: self.icon.position, 0.1: (-130 + offs, self._vpos + 22)},
|
||||
)
|
||||
|
||||
def get_character_name(self) -> str:
|
||||
"""Return the selected character name."""
|
||||
|
|
@ -442,16 +521,14 @@ class Chooser:
|
|||
clamp = False
|
||||
if name == '_random':
|
||||
try:
|
||||
name = (
|
||||
self._sessionplayer.inputdevice.get_default_player_name())
|
||||
name = self._sessionplayer.inputdevice.get_default_player_name()
|
||||
except Exception:
|
||||
print_exception('Error getting _random chooser name.')
|
||||
name = 'Invalid'
|
||||
clamp = not full
|
||||
elif name == '__account__':
|
||||
try:
|
||||
name = self._sessionplayer.inputdevice.get_v1_account_name(
|
||||
full)
|
||||
name = self._sessionplayer.inputdevice.get_v1_account_name(full)
|
||||
except Exception:
|
||||
print_exception('Error getting account name for chooser.')
|
||||
name = 'Invalid'
|
||||
|
|
@ -459,18 +536,21 @@ class Chooser:
|
|||
elif name == '_edit':
|
||||
# Explicitly flattening this to a str; it's only relevant on
|
||||
# the host so that's ok.
|
||||
name = (Lstr(
|
||||
name = Lstr(
|
||||
resource='createEditPlayerText',
|
||||
fallback_resource='editProfileWindow.titleNewText').evaluate())
|
||||
fallback_resource='editProfileWindow.titleNewText',
|
||||
).evaluate()
|
||||
else:
|
||||
# If we have a regular profile marked as global with an icon,
|
||||
# use it (for full only).
|
||||
if full:
|
||||
try:
|
||||
if self._profiles[name_raw].get('global', False):
|
||||
icon = (self._profiles[name_raw]['icon']
|
||||
if 'icon' in self._profiles[name_raw] else
|
||||
_ba.charstr(SpecialChar.LOGO))
|
||||
icon = (
|
||||
self._profiles[name_raw]['icon']
|
||||
if 'icon' in self._profiles[name_raw]
|
||||
else _ba.charstr(SpecialChar.LOGO)
|
||||
)
|
||||
name = icon + name
|
||||
except Exception:
|
||||
print_exception('Error applying global icon.')
|
||||
|
|
@ -488,6 +568,7 @@ class Chooser:
|
|||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.profile import browser as pbrowser
|
||||
from ba._general import Call
|
||||
|
||||
profilename = self._profilenames[self._profileindex]
|
||||
|
||||
# Handle '_edit' as a special case.
|
||||
|
|
@ -503,50 +584,71 @@ class Chooser:
|
|||
if not ready:
|
||||
self._sessionplayer.assigninput(
|
||||
InputType.LEFT_PRESS,
|
||||
Call(self.handlemessage, ChangeMessage('team', -1)))
|
||||
Call(self.handlemessage, ChangeMessage('team', -1)),
|
||||
)
|
||||
self._sessionplayer.assigninput(
|
||||
InputType.RIGHT_PRESS,
|
||||
Call(self.handlemessage, ChangeMessage('team', 1)))
|
||||
Call(self.handlemessage, ChangeMessage('team', 1)),
|
||||
)
|
||||
self._sessionplayer.assigninput(
|
||||
InputType.BOMB_PRESS,
|
||||
Call(self.handlemessage, ChangeMessage('character', 1)))
|
||||
Call(self.handlemessage, ChangeMessage('character', 1)),
|
||||
)
|
||||
self._sessionplayer.assigninput(
|
||||
InputType.UP_PRESS,
|
||||
Call(self.handlemessage, ChangeMessage('profileindex', -1)))
|
||||
Call(self.handlemessage, ChangeMessage('profileindex', -1)),
|
||||
)
|
||||
self._sessionplayer.assigninput(
|
||||
InputType.DOWN_PRESS,
|
||||
Call(self.handlemessage, ChangeMessage('profileindex', 1)))
|
||||
Call(self.handlemessage, ChangeMessage('profileindex', 1)),
|
||||
)
|
||||
self._sessionplayer.assigninput(
|
||||
(InputType.JUMP_PRESS, InputType.PICK_UP_PRESS,
|
||||
InputType.PUNCH_PRESS),
|
||||
Call(self.handlemessage, ChangeMessage('ready', 1)))
|
||||
(
|
||||
InputType.JUMP_PRESS,
|
||||
InputType.PICK_UP_PRESS,
|
||||
InputType.PUNCH_PRESS,
|
||||
),
|
||||
Call(self.handlemessage, ChangeMessage('ready', 1)),
|
||||
)
|
||||
self._ready = False
|
||||
self._update_text()
|
||||
self._sessionplayer.setname('untitled', real=False)
|
||||
else:
|
||||
self._sessionplayer.assigninput(
|
||||
(InputType.LEFT_PRESS, InputType.RIGHT_PRESS,
|
||||
InputType.UP_PRESS, InputType.DOWN_PRESS,
|
||||
InputType.JUMP_PRESS, InputType.BOMB_PRESS,
|
||||
InputType.PICK_UP_PRESS), self._do_nothing)
|
||||
(
|
||||
InputType.LEFT_PRESS,
|
||||
InputType.RIGHT_PRESS,
|
||||
InputType.UP_PRESS,
|
||||
InputType.DOWN_PRESS,
|
||||
InputType.JUMP_PRESS,
|
||||
InputType.BOMB_PRESS,
|
||||
InputType.PICK_UP_PRESS,
|
||||
),
|
||||
self._do_nothing,
|
||||
)
|
||||
self._sessionplayer.assigninput(
|
||||
(InputType.JUMP_PRESS, InputType.BOMB_PRESS,
|
||||
InputType.PICK_UP_PRESS, InputType.PUNCH_PRESS),
|
||||
Call(self.handlemessage, ChangeMessage('ready', 0)))
|
||||
(
|
||||
InputType.JUMP_PRESS,
|
||||
InputType.BOMB_PRESS,
|
||||
InputType.PICK_UP_PRESS,
|
||||
InputType.PUNCH_PRESS,
|
||||
),
|
||||
Call(self.handlemessage, ChangeMessage('ready', 0)),
|
||||
)
|
||||
|
||||
# Store the last profile picked by this input for reuse.
|
||||
input_device = self._sessionplayer.inputdevice
|
||||
name = input_device.name
|
||||
unique_id = input_device.unique_identifier
|
||||
device_profiles = _ba.app.config.setdefault(
|
||||
'Default Player Profiles', {})
|
||||
'Default Player Profiles', {}
|
||||
)
|
||||
|
||||
# Make an exception if we have no custom profiles and are set
|
||||
# to random; in that case we'll want to start picking up custom
|
||||
# profiles if/when one is made so keep our setting cleared.
|
||||
special = ('_random', '_edit', '__account__')
|
||||
have_custom_profiles = any(p not in special
|
||||
for p in self._profiles)
|
||||
have_custom_profiles = any(p not in special for p in self._profiles)
|
||||
|
||||
profilekey = name + ' ' + unique_id
|
||||
if profilename == '_random' and not have_custom_profiles:
|
||||
|
|
@ -557,9 +659,9 @@ class Chooser:
|
|||
_ba.app.config.commit()
|
||||
|
||||
# Set this player's short and full name.
|
||||
self._sessionplayer.setname(self._getname(),
|
||||
self._getname(full=True),
|
||||
real=True)
|
||||
self._sessionplayer.setname(
|
||||
self._getname(), self._getname(full=True), real=True
|
||||
)
|
||||
self._ready = True
|
||||
self._update_text()
|
||||
|
||||
|
|
@ -583,18 +685,21 @@ class Chooser:
|
|||
team_player_counts = {}
|
||||
for sessionteam in sessionteams:
|
||||
team_player_counts[sessionteam.id] = len(
|
||||
sessionteam.players)
|
||||
sessionteam.players
|
||||
)
|
||||
for chooser in lobby.choosers:
|
||||
if chooser.ready:
|
||||
team_player_counts[chooser.sessionteam.id] += 1
|
||||
largest_team_size = max(team_player_counts.values())
|
||||
smallest_team_size = (min(team_player_counts.values()))
|
||||
smallest_team_size = min(team_player_counts.values())
|
||||
|
||||
# Force switch if we're on the biggest sessionteam
|
||||
# and there's a smaller one available.
|
||||
if (largest_team_size != smallest_team_size
|
||||
and team_player_counts[self.sessionteam.id] >=
|
||||
largest_team_size):
|
||||
if (
|
||||
largest_team_size != smallest_team_size
|
||||
and team_player_counts[self.sessionteam.id]
|
||||
>= largest_team_size
|
||||
):
|
||||
force_team_switch = True
|
||||
|
||||
# Either force switch teams, or actually for realsies do the set-ready.
|
||||
|
|
@ -612,8 +717,7 @@ class Chooser:
|
|||
if now - self._last_change[0] < QUICK_CHANGE_INTERVAL:
|
||||
count += 1
|
||||
if count > MAX_QUICK_CHANGE_COUNT:
|
||||
_ba.disconnect_client(
|
||||
self._sessionplayer.inputdevice.client_id)
|
||||
_ba.disconnect_client(self._sessionplayer.inputdevice.client_id)
|
||||
elif now - self._last_change[0] > QUICK_CHANGE_RESET_INTERVAL:
|
||||
count = 0
|
||||
self._last_change = (now, count)
|
||||
|
|
@ -638,8 +742,8 @@ class Chooser:
|
|||
if len(sessionteams) > 1:
|
||||
_ba.playsound(self._swish_sound)
|
||||
self._selected_team_index = (
|
||||
(self._selected_team_index + msg.value) %
|
||||
len(sessionteams))
|
||||
self._selected_team_index + msg.value
|
||||
) % len(sessionteams)
|
||||
self._update_text()
|
||||
self.update_position()
|
||||
self._update_icon()
|
||||
|
|
@ -655,15 +759,17 @@ class Chooser:
|
|||
# Pick the next player profile and assign our name
|
||||
# and character based on that.
|
||||
_ba.playsound(self._deek_sound)
|
||||
self._profileindex = ((self._profileindex + msg.value) %
|
||||
len(self._profilenames))
|
||||
self._profileindex = (self._profileindex + msg.value) % len(
|
||||
self._profilenames
|
||||
)
|
||||
self.update_from_profile()
|
||||
|
||||
elif msg.what == 'character':
|
||||
_ba.playsound(self._click_sound)
|
||||
# update our index in our local list of characters
|
||||
self._character_index = ((self._character_index + msg.value) %
|
||||
len(self._character_names))
|
||||
self._character_index = (
|
||||
self._character_index + msg.value
|
||||
) % len(self._character_names)
|
||||
self._update_text()
|
||||
self._update_icon()
|
||||
|
||||
|
|
@ -677,30 +783,34 @@ class Chooser:
|
|||
# Once we're ready, we've saved the name, so lets ask the system
|
||||
# for it so we get appended numbers and stuff.
|
||||
text = Lstr(value=self._sessionplayer.getname(full=True))
|
||||
text = Lstr(value='${A} (${B})',
|
||||
subs=[('${A}', text),
|
||||
('${B}', Lstr(resource='readyText'))])
|
||||
text = Lstr(
|
||||
value='${A} (${B})',
|
||||
subs=[('${A}', text), ('${B}', Lstr(resource='readyText'))],
|
||||
)
|
||||
else:
|
||||
text = Lstr(value=self._getname(full=True))
|
||||
|
||||
can_switch_teams = len(self.lobby.sessionteams) > 1
|
||||
|
||||
# Flash as we're coming in.
|
||||
fin_color = _ba.safecolor(self.get_color()) + (1, )
|
||||
fin_color = _ba.safecolor(self.get_color()) + (1,)
|
||||
if not self._inited:
|
||||
animate_array(self._text_node, 'color', 4, {
|
||||
0.15: fin_color,
|
||||
0.25: (2, 2, 2, 1),
|
||||
0.35: fin_color
|
||||
})
|
||||
animate_array(
|
||||
self._text_node,
|
||||
'color',
|
||||
4,
|
||||
{0.15: fin_color, 0.25: (2, 2, 2, 1), 0.35: fin_color},
|
||||
)
|
||||
else:
|
||||
|
||||
# Blend if we're in teams mode; switch instantly otherwise.
|
||||
if can_switch_teams:
|
||||
animate_array(self._text_node, 'color', 4, {
|
||||
0: self._text_node.color,
|
||||
0.1: fin_color
|
||||
})
|
||||
animate_array(
|
||||
self._text_node,
|
||||
'color',
|
||||
4,
|
||||
{0: self._text_node.color, 0.1: fin_color},
|
||||
)
|
||||
else:
|
||||
self._text_node.color = fin_color
|
||||
|
||||
|
|
@ -740,9 +850,11 @@ class Chooser:
|
|||
max_val = sessionteam.color[j]
|
||||
max_index = j
|
||||
that_color_for_us = highlight[max_index]
|
||||
our_second_biggest = max(highlight[(max_index + 1) % 3],
|
||||
highlight[(max_index + 2) % 3])
|
||||
diff = (that_color_for_us - our_second_biggest)
|
||||
our_second_biggest = max(
|
||||
highlight[(max_index + 1) % 3],
|
||||
highlight[(max_index + 2) % 3],
|
||||
)
|
||||
diff = that_color_for_us - our_second_biggest
|
||||
if diff > 0:
|
||||
highlight[max_index] -= diff * 0.6
|
||||
highlight[(max_index + 1) % 3] += diff * 0.3
|
||||
|
|
@ -764,10 +876,12 @@ class Chooser:
|
|||
return
|
||||
|
||||
try:
|
||||
tex_name = (_ba.app.spaz_appearances[self._character_names[
|
||||
self._character_index]].icon_texture)
|
||||
tint_tex_name = (_ba.app.spaz_appearances[self._character_names[
|
||||
self._character_index]].icon_mask_texture)
|
||||
tex_name = _ba.app.spaz_appearances[
|
||||
self._character_names[self._character_index]
|
||||
].icon_texture
|
||||
tint_tex_name = _ba.app.spaz_appearances[
|
||||
self._character_names[self._character_index]
|
||||
].icon_mask_texture
|
||||
except Exception:
|
||||
print_exception('Error updating char icon list')
|
||||
tex_name = 'neoSpazIcon'
|
||||
|
|
@ -786,18 +900,18 @@ class Chooser:
|
|||
|
||||
# If we're initing, flash.
|
||||
if not self._inited:
|
||||
animate_array(self.icon, 'color', 3, {
|
||||
0.15: (1, 1, 1),
|
||||
0.25: (2, 2, 2),
|
||||
0.35: (1, 1, 1)
|
||||
})
|
||||
animate_array(
|
||||
self.icon,
|
||||
'color',
|
||||
3,
|
||||
{0.15: (1, 1, 1), 0.25: (2, 2, 2), 0.35: (1, 1, 1)},
|
||||
)
|
||||
|
||||
# Blend in teams mode; switch instantly in ffa-mode.
|
||||
if can_switch_teams:
|
||||
animate_array(self.icon, 'tint_color', 3, {
|
||||
0: self.icon.tint_color,
|
||||
0.1: clr
|
||||
})
|
||||
animate_array(
|
||||
self.icon, 'tint_color', 3, {0: self.icon.tint_color, 0.1: clr}
|
||||
)
|
||||
else:
|
||||
self.icon.tint_color = clr
|
||||
self.icon.tint2_color = clr2
|
||||
|
|
@ -825,6 +939,7 @@ class Lobby:
|
|||
def __init__(self) -> None:
|
||||
from ba._team import SessionTeam
|
||||
from ba._coopsession import CoopSession
|
||||
|
||||
session = _ba.getsession()
|
||||
self._use_team_colors = session.use_team_colors
|
||||
if session.use_teams:
|
||||
|
|
@ -834,7 +949,7 @@ class Lobby:
|
|||
else:
|
||||
self._dummy_teams = SessionTeam()
|
||||
self._sessionteams = [weakref.ref(self._dummy_teams)]
|
||||
v_offset = (-150 if isinstance(session, CoopSession) else -50)
|
||||
v_offset = -150 if isinstance(session, CoopSession) else -50
|
||||
self.choosers: list[Chooser] = []
|
||||
self.base_v_offset = v_offset
|
||||
self.update_positions()
|
||||
|
|
@ -916,9 +1031,11 @@ class Lobby:
|
|||
def add_chooser(self, sessionplayer: ba.SessionPlayer) -> None:
|
||||
"""Add a chooser to the lobby for the provided player."""
|
||||
self.choosers.append(
|
||||
Chooser(vpos=self._vpos, sessionplayer=sessionplayer, lobby=self))
|
||||
Chooser(vpos=self._vpos, sessionplayer=sessionplayer, lobby=self)
|
||||
)
|
||||
self._next_add_team = (self._next_add_team + 1) % len(
|
||||
self._sessionteams)
|
||||
self._sessionteams
|
||||
)
|
||||
self._vpos -= 48
|
||||
|
||||
def remove_chooser(self, player: ba.SessionPlayer) -> None:
|
||||
|
|
|
|||
130
dist/ba_data/python/ba/_map.py
vendored
130
dist/ba_data/python/ba/_map.py
vendored
|
|
@ -49,6 +49,7 @@ def get_map_display_string(name: str) -> ba.Lstr:
|
|||
Category: **Asset Functions**
|
||||
"""
|
||||
from ba import _language
|
||||
|
||||
return _language.Lstr(translate=('mapsNames', name))
|
||||
|
||||
|
||||
|
|
@ -97,8 +98,11 @@ def getmaps(playtype: str) -> list[str]:
|
|||
For racing games where players much touch each region in order.
|
||||
Has two or more 'race_point' locations.
|
||||
"""
|
||||
return sorted(key for key, val in _ba.app.maps.items()
|
||||
if playtype in val.get_play_types())
|
||||
return sorted(
|
||||
key
|
||||
for key, val in _ba.app.maps.items()
|
||||
if playtype in val.get_play_types()
|
||||
)
|
||||
|
||||
|
||||
def get_map_class(name: str) -> type[ba.Map]:
|
||||
|
|
@ -111,6 +115,7 @@ def get_map_class(name: str) -> type[ba.Map]:
|
|||
return _ba.app.maps[name]
|
||||
except KeyError:
|
||||
from ba import _error
|
||||
|
||||
raise _error.NotFoundError(f"Map not found: '{name}'") from None
|
||||
|
||||
|
||||
|
|
@ -122,6 +127,7 @@ class Map(Actor):
|
|||
Consists of a collection of terrain nodes, metadata, and other
|
||||
functionality comprising a game map.
|
||||
"""
|
||||
|
||||
defs: Any = None
|
||||
name = 'Map'
|
||||
_playtypes: list[str] = []
|
||||
|
|
@ -170,8 +176,9 @@ class Map(Actor):
|
|||
"""
|
||||
return None
|
||||
|
||||
def __init__(self,
|
||||
vr_overlay_offset: Sequence[float] | None = None) -> None:
|
||||
def __init__(
|
||||
self, vr_overlay_offset: Sequence[float] | None = None
|
||||
) -> None:
|
||||
"""Instantiate a map."""
|
||||
super().__init__()
|
||||
|
||||
|
|
@ -185,10 +192,13 @@ class Map(Actor):
|
|||
self.preloaddata = _ba.getactivity().preloads[type(self)]
|
||||
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
|
||||
'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 = _ba.getactivity().globalsnode
|
||||
|
|
@ -214,9 +224,12 @@ class Map(Actor):
|
|||
# Set shadow ranges.
|
||||
try:
|
||||
gnode.shadow_range = [
|
||||
self.defs.points[v][1] for v in [
|
||||
'shadow_lower_bottom', 'shadow_lower_top',
|
||||
'shadow_upper_bottom', 'shadow_upper_top'
|
||||
self.defs.points[v][1]
|
||||
for v in [
|
||||
'shadow_lower_bottom',
|
||||
'shadow_lower_top',
|
||||
'shadow_upper_bottom',
|
||||
'shadow_upper_top',
|
||||
]
|
||||
]
|
||||
except Exception:
|
||||
|
|
@ -224,36 +237,42 @@ class Map(Actor):
|
|||
|
||||
# In vr, set a fixed point in space for the overlay to show up at.
|
||||
# By default we use the bounds center but allow the map to override it.
|
||||
center = ((aoi_bounds[0] + aoi_bounds[3]) * 0.5,
|
||||
(aoi_bounds[1] + aoi_bounds[4]) * 0.5,
|
||||
(aoi_bounds[2] + aoi_bounds[5]) * 0.5)
|
||||
center = (
|
||||
(aoi_bounds[0] + aoi_bounds[3]) * 0.5,
|
||||
(aoi_bounds[1] + aoi_bounds[4]) * 0.5,
|
||||
(aoi_bounds[2] + aoi_bounds[5]) * 0.5,
|
||||
)
|
||||
if vr_overlay_offset is not None:
|
||||
center = (center[0] + vr_overlay_offset[0],
|
||||
center[1] + vr_overlay_offset[1],
|
||||
center[2] + vr_overlay_offset[2])
|
||||
center = (
|
||||
center[0] + vr_overlay_offset[0],
|
||||
center[1] + vr_overlay_offset[1],
|
||||
center[2] + vr_overlay_offset[2],
|
||||
)
|
||||
gnode.vr_overlay_center = center
|
||||
gnode.vr_overlay_center_enabled = True
|
||||
|
||||
self.spawn_points = (self.get_def_points('spawn')
|
||||
or [(0, 0, 0, 0, 0, 0)])
|
||||
self.ffa_spawn_points = (self.get_def_points('ffa_spawn')
|
||||
or [(0, 0, 0, 0, 0, 0)])
|
||||
self.spawn_by_flag_points = (self.get_def_points('spawn_by_flag')
|
||||
or [(0, 0, 0, 0, 0, 0)])
|
||||
self.spawn_points = self.get_def_points('spawn') or [(0, 0, 0, 0, 0, 0)]
|
||||
self.ffa_spawn_points = self.get_def_points('ffa_spawn') or [
|
||||
(0, 0, 0, 0, 0, 0)
|
||||
]
|
||||
self.spawn_by_flag_points = self.get_def_points('spawn_by_flag') or [
|
||||
(0, 0, 0, 0, 0, 0)
|
||||
]
|
||||
self.flag_points = self.get_def_points('flag') or [(0, 0, 0)]
|
||||
|
||||
# We just want points.
|
||||
self.flag_points = [p[:3] for p in self.flag_points]
|
||||
self.flag_points_default = (self.get_def_point('flag_default')
|
||||
or (0, 1, 0))
|
||||
self.flag_points_default = self.get_def_point('flag_default') or (
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
)
|
||||
self.powerup_spawn_points = self.get_def_points('powerup_spawn') or [
|
||||
(0, 0, 0)
|
||||
]
|
||||
|
||||
# We just want points.
|
||||
self.powerup_spawn_points = ([
|
||||
p[:3] for p in self.powerup_spawn_points
|
||||
])
|
||||
self.powerup_spawn_points = [p[:3] for p in self.powerup_spawn_points]
|
||||
self.tnt_points = self.get_def_points('tnt') or []
|
||||
|
||||
# We just want points.
|
||||
|
|
@ -266,11 +285,10 @@ class Map(Actor):
|
|||
# Let's select random index for first spawn point,
|
||||
# so that no one is offended by the constant spawn on the edge.
|
||||
self._next_ffa_start_index = random.randrange(
|
||||
len(self.ffa_spawn_points))
|
||||
len(self.ffa_spawn_points)
|
||||
)
|
||||
|
||||
def is_point_near_edge(self,
|
||||
point: ba.Vec3,
|
||||
running: bool = False) -> bool:
|
||||
def is_point_near_edge(self, point: ba.Vec3, running: bool = False) -> bool:
|
||||
"""Return whether the provided point is near an edge of the map.
|
||||
|
||||
Simple bot logic uses this call to determine if they
|
||||
|
|
@ -282,22 +300,32 @@ class Map(Actor):
|
|||
return False
|
||||
|
||||
def get_def_bound_box(
|
||||
self, name: str
|
||||
self, name: str
|
||||
) -> tuple[float, float, float, float, float, float] | None:
|
||||
"""Return a 6 member bounds tuple or None if it is not defined."""
|
||||
try:
|
||||
box = self.defs.boxes[name]
|
||||
return (box[0] - box[6] / 2.0, box[1] - box[7] / 2.0,
|
||||
box[2] - box[8] / 2.0, box[0] + box[6] / 2.0,
|
||||
box[1] + box[7] / 2.0, box[2] + box[8] / 2.0)
|
||||
return (
|
||||
box[0] - box[6] / 2.0,
|
||||
box[1] - box[7] / 2.0,
|
||||
box[2] - box[8] / 2.0,
|
||||
box[0] + box[6] / 2.0,
|
||||
box[1] + box[7] / 2.0,
|
||||
box[2] + box[8] / 2.0,
|
||||
)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_def_point(self, name: str) -> Sequence[float] | None:
|
||||
"""Return a single defined point or a default value in its absence."""
|
||||
val = self.defs.points.get(name)
|
||||
return (None if val is None else
|
||||
_math.vec3validate(val) if __debug__ else val)
|
||||
return (
|
||||
None
|
||||
if val is None
|
||||
else _math.vec3validate(val)
|
||||
if __debug__
|
||||
else val
|
||||
)
|
||||
|
||||
def get_def_points(self, name: str) -> list[Sequence[float]]:
|
||||
"""Return a list of named points.
|
||||
|
|
@ -324,12 +352,16 @@ class Map(Actor):
|
|||
pnt = self.spawn_points[team_index % len(self.spawn_points)]
|
||||
x_range = (-0.5, 0.5) if pnt[3] == 0.0 else (-pnt[3], pnt[3])
|
||||
z_range = (-0.5, 0.5) if pnt[5] == 0.0 else (-pnt[5], pnt[5])
|
||||
pnt = (pnt[0] + random.uniform(*x_range), pnt[1],
|
||||
pnt[2] + random.uniform(*z_range))
|
||||
pnt = (
|
||||
pnt[0] + random.uniform(*x_range),
|
||||
pnt[1],
|
||||
pnt[2] + random.uniform(*z_range),
|
||||
)
|
||||
return pnt
|
||||
|
||||
def get_ffa_start_position(
|
||||
self, players: Sequence[ba.Player]) -> Sequence[float]:
|
||||
self, players: Sequence[ba.Player]
|
||||
) -> Sequence[float]:
|
||||
"""Return a random starting position in one of the FFA spawn areas.
|
||||
|
||||
If a list of ba.Player-s is provided; the returned points will be
|
||||
|
|
@ -344,12 +376,16 @@ class Map(Actor):
|
|||
|
||||
def _getpt() -> Sequence[float]:
|
||||
point = self.ffa_spawn_points[self._next_ffa_start_index]
|
||||
self._next_ffa_start_index = ((self._next_ffa_start_index + 1) %
|
||||
len(self.ffa_spawn_points))
|
||||
self._next_ffa_start_index = (self._next_ffa_start_index + 1) % len(
|
||||
self.ffa_spawn_points
|
||||
)
|
||||
x_range = (-0.5, 0.5) if point[3] == 0.0 else (-point[3], point[3])
|
||||
z_range = (-0.5, 0.5) if point[5] == 0.0 else (-point[5], point[5])
|
||||
point = (point[0] + random.uniform(*x_range), point[1],
|
||||
point[2] + random.uniform(*z_range))
|
||||
point = (
|
||||
point[0] + random.uniform(*x_range),
|
||||
point[1],
|
||||
point[2] + random.uniform(*z_range),
|
||||
)
|
||||
return point
|
||||
|
||||
if not player_pts:
|
||||
|
|
@ -372,8 +408,9 @@ class Map(Actor):
|
|||
assert farthestpt is not None
|
||||
return tuple(farthestpt)
|
||||
|
||||
def get_flag_position(self,
|
||||
team_index: int | None = None) -> Sequence[float]:
|
||||
def get_flag_position(
|
||||
self, team_index: int | None = None
|
||||
) -> Sequence[float]:
|
||||
"""Return a flag position on the map for the given team index.
|
||||
|
||||
Pass None to get the default flag point.
|
||||
|
|
@ -388,6 +425,7 @@ class Map(Actor):
|
|||
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
from ba import _messages
|
||||
|
||||
if isinstance(msg, _messages.DieMessage):
|
||||
if self.node:
|
||||
self.node.delete()
|
||||
|
|
|
|||
9
dist/ba_data/python/ba/_math.py
vendored
9
dist/ba_data/python/ba/_math.py
vendored
|
|
@ -24,6 +24,7 @@ def vec3validate(value: Sequence[float]) -> Sequence[float]:
|
|||
to keep runtime overhead minimal.
|
||||
"""
|
||||
from numbers import Number
|
||||
|
||||
if not isinstance(value, abc.Sequence):
|
||||
raise TypeError(f"Expected a sequence; got {type(value)}")
|
||||
if len(value) != 3:
|
||||
|
|
@ -40,9 +41,11 @@ def is_point_in_box(pnt: Sequence[float], box: Sequence[float]) -> bool:
|
|||
|
||||
For use with standard def boxes (position|rotate|scale).
|
||||
"""
|
||||
return ((abs(pnt[0] - box[0]) <= box[6] * 0.5)
|
||||
and (abs(pnt[1] - box[1]) <= box[7] * 0.5)
|
||||
and (abs(pnt[2] - box[2]) <= box[8] * 0.5))
|
||||
return (
|
||||
(abs(pnt[0] - box[0]) <= box[6] * 0.5)
|
||||
and (abs(pnt[1] - box[1]) <= box[7] * 0.5)
|
||||
and (abs(pnt[2] - box[2]) <= box[8] * 0.5)
|
||||
)
|
||||
|
||||
|
||||
def normalized_color(color: Sequence[float]) -> tuple[float, ...]:
|
||||
|
|
|
|||
53
dist/ba_data/python/ba/_messages.py
vendored
53
dist/ba_data/python/ba/_messages.py
vendored
|
|
@ -38,6 +38,7 @@ class DeathType(Enum):
|
|||
|
||||
Category: Enums
|
||||
"""
|
||||
|
||||
GENERIC = 'generic'
|
||||
OUT_OF_BOUNDS = 'out_of_bounds'
|
||||
IMPACT = 'impact'
|
||||
|
|
@ -83,8 +84,13 @@ class PlayerDiedMessage:
|
|||
how: ba.DeathType
|
||||
"""The particular type of death."""
|
||||
|
||||
def __init__(self, player: ba.Player, was_killed: bool,
|
||||
killerplayer: ba.Player | None, how: ba.DeathType):
|
||||
def __init__(
|
||||
self,
|
||||
player: ba.Player,
|
||||
was_killed: bool,
|
||||
killerplayer: ba.Player | None,
|
||||
how: ba.DeathType,
|
||||
):
|
||||
"""Instantiate a message with the given values."""
|
||||
|
||||
# Invalid refs should never be passed as args.
|
||||
|
|
@ -97,8 +103,9 @@ class PlayerDiedMessage:
|
|||
self.killed = was_killed
|
||||
self.how = how
|
||||
|
||||
def getkillerplayer(self,
|
||||
playertype: type[PlayerType]) -> PlayerType | None:
|
||||
def getkillerplayer(
|
||||
self, playertype: type[PlayerType]
|
||||
) -> PlayerType | None:
|
||||
"""Return the ba.Player responsible for the killing, if any.
|
||||
|
||||
Pass the Player type being used by the current game.
|
||||
|
|
@ -235,19 +242,21 @@ class HitMessage:
|
|||
their effect to a target.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
srcnode: ba.Node | None = None,
|
||||
pos: Sequence[float] | None = None,
|
||||
velocity: Sequence[float] | None = None,
|
||||
magnitude: float = 1.0,
|
||||
velocity_magnitude: float = 0.0,
|
||||
radius: float = 1.0,
|
||||
source_player: ba.Player | None = None,
|
||||
kick_back: float = 1.0,
|
||||
flat_damage: float | None = None,
|
||||
hit_type: str = 'generic',
|
||||
force_direction: Sequence[float] | None = None,
|
||||
hit_subtype: str = 'default'):
|
||||
def __init__(
|
||||
self,
|
||||
srcnode: ba.Node | None = None,
|
||||
pos: Sequence[float] | None = None,
|
||||
velocity: Sequence[float] | None = None,
|
||||
magnitude: float = 1.0,
|
||||
velocity_magnitude: float = 0.0,
|
||||
radius: float = 1.0,
|
||||
source_player: ba.Player | None = None,
|
||||
kick_back: float = 1.0,
|
||||
flat_damage: float | None = None,
|
||||
hit_type: str = 'generic',
|
||||
force_direction: Sequence[float] | None = None,
|
||||
hit_subtype: str = 'default',
|
||||
):
|
||||
"""Instantiate a message with given values."""
|
||||
|
||||
self.srcnode = srcnode
|
||||
|
|
@ -264,11 +273,13 @@ class HitMessage:
|
|||
self.flat_damage = flat_damage
|
||||
self.hit_type = hit_type
|
||||
self.hit_subtype = hit_subtype
|
||||
self.force_direction = (force_direction
|
||||
if force_direction is not None else velocity)
|
||||
self.force_direction = (
|
||||
force_direction if force_direction is not None else velocity
|
||||
)
|
||||
|
||||
def get_source_player(self,
|
||||
playertype: type[PlayerType]) -> PlayerType | None:
|
||||
def get_source_player(
|
||||
self, playertype: type[PlayerType]
|
||||
) -> PlayerType | None:
|
||||
"""Return the source-player if one exists and is the provided type."""
|
||||
player: Any = self._source_player
|
||||
|
||||
|
|
|
|||
123
dist/ba_data/python/ba/_meta.py
vendored
123
dist/ba_data/python/ba/_meta.py
vendored
|
|
@ -43,6 +43,7 @@ T = TypeVar('T')
|
|||
@dataclass
|
||||
class ScanResults:
|
||||
"""Final results from a meta-scan."""
|
||||
|
||||
exports: dict[str, list[str]] = field(default_factory=dict)
|
||||
errors: list[str] = field(default_factory=list)
|
||||
warnings: list[str] = field(default_factory=list)
|
||||
|
|
@ -84,7 +85,8 @@ class MetadataSubsystem:
|
|||
|
||||
self._scan_complete_cb = scan_complete_cb
|
||||
self._scan = DirectoryScan(
|
||||
[_ba.app.python_directory_app, _ba.app.python_directory_user])
|
||||
[_ba.app.python_directory_app, _ba.app.python_directory_user]
|
||||
)
|
||||
|
||||
Thread(target=self._run_scan_in_bg, daemon=True).start()
|
||||
|
||||
|
|
@ -115,8 +117,12 @@ class MetadataSubsystem:
|
|||
loading work happens, pass completion_cb_in_bg_thread=True.
|
||||
"""
|
||||
Thread(
|
||||
target=tpartial(self._load_exported_classes, cls, completion_cb,
|
||||
completion_cb_in_bg_thread),
|
||||
target=tpartial(
|
||||
self._load_exported_classes,
|
||||
cls,
|
||||
completion_cb,
|
||||
completion_cb_in_bg_thread,
|
||||
),
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
|
|
@ -127,6 +133,7 @@ class MetadataSubsystem:
|
|||
completion_cb_in_bg_thread: bool,
|
||||
) -> None:
|
||||
from ba._general import getclass
|
||||
|
||||
classes: list[type[T]] = []
|
||||
try:
|
||||
classnames = self._wait_for_scan_results().exports_of_class(cls)
|
||||
|
|
@ -152,7 +159,8 @@ class MetadataSubsystem:
|
|||
logging.warning(
|
||||
'ba.meta._wait_for_scan_results()'
|
||||
' called in logic thread before scan completed;'
|
||||
' this can cause hitches.')
|
||||
' this can cause hitches.'
|
||||
)
|
||||
|
||||
# Now wait a bit for the scan to complete.
|
||||
# Eventually error though if it doesn't.
|
||||
|
|
@ -161,7 +169,8 @@ class MetadataSubsystem:
|
|||
time.sleep(0.05)
|
||||
if time.time() - starttime > 10.0:
|
||||
raise TimeoutError(
|
||||
'timeout waiting for meta scan to complete.')
|
||||
'timeout waiting for meta scan to complete.'
|
||||
)
|
||||
return self.scanresults
|
||||
|
||||
def _run_scan_in_bg(self) -> None:
|
||||
|
|
@ -181,6 +190,7 @@ class MetadataSubsystem:
|
|||
def _handle_scan_results(self) -> None:
|
||||
"""Called in the logic thread with results of a completed scan."""
|
||||
from ba._language import Lstr
|
||||
|
||||
assert _ba.in_logic_thread()
|
||||
|
||||
results = self.scanresults
|
||||
|
|
@ -192,16 +202,20 @@ class MetadataSubsystem:
|
|||
# Errors are more serious and will get included in the regular log.
|
||||
if results.warnings or results.errors:
|
||||
import textwrap
|
||||
_ba.screenmessage(Lstr(resource='scanScriptsErrorText'),
|
||||
color=(1, 0, 0))
|
||||
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='scanScriptsErrorText'), color=(1, 0, 0)
|
||||
)
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
if results.warnings:
|
||||
allwarnings = textwrap.indent('\n'.join(results.warnings),
|
||||
'Warning (meta-scan): ')
|
||||
allwarnings = textwrap.indent(
|
||||
'\n'.join(results.warnings), 'Warning (meta-scan): '
|
||||
)
|
||||
logging.warning(allwarnings)
|
||||
if results.errors:
|
||||
allerrors = textwrap.indent('\n'.join(results.errors),
|
||||
'Error (meta-scan): ')
|
||||
allerrors = textwrap.indent(
|
||||
'\n'.join(results.errors), 'Error (meta-scan): '
|
||||
)
|
||||
logging.error(allerrors)
|
||||
|
||||
# Let the game know we're done.
|
||||
|
|
@ -248,23 +262,28 @@ class DirectoryScan:
|
|||
self._scan_module(moduledir, subpath)
|
||||
except Exception:
|
||||
import traceback
|
||||
|
||||
self.results.warnings.append(
|
||||
f"Error scanning '{subpath}': " +
|
||||
traceback.format_exc())
|
||||
f"Error scanning '{subpath}': " + traceback.format_exc()
|
||||
)
|
||||
|
||||
# Sort our results
|
||||
for exportlist in self.results.exports.values():
|
||||
exportlist.sort()
|
||||
|
||||
def _get_path_module_entries(self, path: Path, subpath: str | Path,
|
||||
modules: list[tuple[Path, Path]]) -> None:
|
||||
def _get_path_module_entries(
|
||||
self, path: Path, subpath: str | Path, modules: list[tuple[Path, Path]]
|
||||
) -> None:
|
||||
"""Scan provided path and add module entries to provided list."""
|
||||
try:
|
||||
# Special case: let's save some time and skip the whole 'ba'
|
||||
# package since we know it doesn't contain any meta tags.
|
||||
fullpath = Path(path, subpath)
|
||||
entries = [(path, Path(subpath, name))
|
||||
for name in os.listdir(fullpath) if name != 'ba']
|
||||
entries = [
|
||||
(path, Path(subpath, name))
|
||||
for name in os.listdir(fullpath)
|
||||
if name != 'ba'
|
||||
]
|
||||
except PermissionError:
|
||||
# Expected sometimes.
|
||||
entries = []
|
||||
|
|
@ -277,8 +296,10 @@ class DirectoryScan:
|
|||
for entry in entries:
|
||||
if entry[1].name.endswith('.py'):
|
||||
modules.append(entry)
|
||||
elif (Path(entry[0], entry[1]).is_dir()
|
||||
and Path(entry[0], entry[1], '__init__.py').is_file()):
|
||||
elif (
|
||||
Path(entry[0], entry[1]).is_dir()
|
||||
and Path(entry[0], entry[1], '__init__.py').is_file()
|
||||
):
|
||||
modules.append(entry)
|
||||
|
||||
def _scan_module(self, moduledir: Path, subpath: Path) -> None:
|
||||
|
|
@ -293,11 +314,13 @@ class DirectoryScan:
|
|||
flines = infile.readlines()
|
||||
meta_lines = {
|
||||
lnum: l[1:].split()
|
||||
for lnum, l in enumerate(flines) if '# ba_meta ' in l
|
||||
for lnum, l in enumerate(flines)
|
||||
if '# ba_meta ' in l
|
||||
}
|
||||
is_top_level = len(subpath.parts) <= 1
|
||||
required_api = self._get_api_requirement(subpath, meta_lines,
|
||||
is_top_level)
|
||||
required_api = self._get_api_requirement(
|
||||
subpath, meta_lines, is_top_level
|
||||
)
|
||||
|
||||
# Top level modules with no discernible api version get ignored.
|
||||
if is_top_level and required_api is None:
|
||||
|
|
@ -308,7 +331,8 @@ class DirectoryScan:
|
|||
if required_api is not None and required_api < CURRENT_API_VERSION:
|
||||
self.results.warnings += (
|
||||
f'Warning: {subpath} requires api {required_api} but'
|
||||
f' we are running {CURRENT_API_VERSION}; ignoring module.')
|
||||
f' we are running {CURRENT_API_VERSION}; ignoring module.'
|
||||
)
|
||||
return
|
||||
|
||||
# Ok; can proceed with a full scan of this module.
|
||||
|
|
@ -324,11 +348,14 @@ class DirectoryScan:
|
|||
self._scan_module(submodule[0], submodule[1])
|
||||
except Exception:
|
||||
import traceback
|
||||
self.results.warnings.append(
|
||||
f"Error scanning '{subpath}': {traceback.format_exc()}")
|
||||
|
||||
def _process_module_meta_tags(self, subpath: Path, flines: list[str],
|
||||
meta_lines: dict[int, list[str]]) -> None:
|
||||
self.results.warnings.append(
|
||||
f"Error scanning '{subpath}': {traceback.format_exc()}"
|
||||
)
|
||||
|
||||
def _process_module_meta_tags(
|
||||
self, subpath: Path, flines: list[str], meta_lines: dict[int, list[str]]
|
||||
) -> None:
|
||||
"""Pull data from a module based on its ba_meta tags."""
|
||||
for lindex, mline in meta_lines.items():
|
||||
# meta_lines is just anything containing '# ba_meta '; make sure
|
||||
|
|
@ -336,9 +363,11 @@ class DirectoryScan:
|
|||
if mline[0] != 'ba_meta':
|
||||
self.results.warnings.append(
|
||||
f'Warning: {subpath}:'
|
||||
f' malformed ba_meta statement on line {lindex + 1}.')
|
||||
elif (len(mline) == 4 and mline[1] == 'require'
|
||||
and mline[2] == 'api'):
|
||||
f' malformed ba_meta statement on line {lindex + 1}.'
|
||||
)
|
||||
elif (
|
||||
len(mline) == 4 and mline[1] == 'require' and mline[2] == 'api'
|
||||
):
|
||||
# Ignore 'require api X' lines in this pass.
|
||||
pass
|
||||
elif len(mline) != 3 or mline[1] != 'export':
|
||||
|
|
@ -346,7 +375,8 @@ class DirectoryScan:
|
|||
# complain for anything else we see.
|
||||
self.results.warnings.append(
|
||||
f'Warning: {subpath}'
|
||||
f': unrecognized ba_meta statement on line {lindex + 1}.')
|
||||
f': unrecognized ba_meta statement on line {lindex + 1}.'
|
||||
)
|
||||
else:
|
||||
# Looks like we've got a valid export line!
|
||||
modulename = '.'.join(subpath.parts)
|
||||
|
|
@ -354,7 +384,8 @@ class DirectoryScan:
|
|||
modulename = modulename[:-3]
|
||||
exporttypestr = mline[2]
|
||||
export_class_name = self._get_export_class_name(
|
||||
subpath, flines, lindex)
|
||||
subpath, flines, lindex
|
||||
)
|
||||
if export_class_name is not None:
|
||||
classname = modulename + '.' + export_class_name
|
||||
|
||||
|
|
@ -364,11 +395,13 @@ class DirectoryScan:
|
|||
exporttype = EXPORT_CLASS_NAME_SHORTCUTS.get(exporttypestr)
|
||||
if exporttype is None:
|
||||
exporttype = exporttypestr
|
||||
self.results.exports.setdefault(exporttype,
|
||||
[]).append(classname)
|
||||
self.results.exports.setdefault(exporttype, []).append(
|
||||
classname
|
||||
)
|
||||
|
||||
def _get_export_class_name(self, subpath: Path, lines: list[str],
|
||||
lindex: int) -> str | None:
|
||||
def _get_export_class_name(
|
||||
self, subpath: Path, lines: list[str], lindex: int
|
||||
) -> str | None:
|
||||
"""Given line num of an export tag, returns its operand class name."""
|
||||
lindexorig = lindex
|
||||
classname = None
|
||||
|
|
@ -389,7 +422,8 @@ class DirectoryScan:
|
|||
if classname is None:
|
||||
self.results.warnings.append(
|
||||
f'Warning: {subpath}: class definition not found below'
|
||||
f' "ba_meta export" statement on line {lindexorig + 1}.')
|
||||
f' "ba_meta export" statement on line {lindexorig + 1}.'
|
||||
)
|
||||
return classname
|
||||
|
||||
def _get_api_requirement(
|
||||
|
|
@ -403,8 +437,13 @@ class DirectoryScan:
|
|||
Malformed api requirement strings will be logged as warnings.
|
||||
"""
|
||||
lines = [
|
||||
l for l in meta_lines.values() if len(l) == 4 and l[0] == 'ba_meta'
|
||||
and l[1] == 'require' and l[2] == 'api' and l[3].isdigit()
|
||||
l
|
||||
for l in meta_lines.values()
|
||||
if len(l) == 4
|
||||
and l[0] == 'ba_meta'
|
||||
and l[1] == 'require'
|
||||
and l[2] == 'api'
|
||||
and l[3].isdigit()
|
||||
]
|
||||
|
||||
# We're successful if we find exactly one properly formatted line.
|
||||
|
|
@ -416,12 +455,14 @@ class DirectoryScan:
|
|||
self.results.warnings.append(
|
||||
f'Warning: {subpath}: multiple'
|
||||
' "# ba_meta require api <NUM>" lines found;'
|
||||
' ignoring module.')
|
||||
' ignoring module.'
|
||||
)
|
||||
elif not lines and toplevel and meta_lines:
|
||||
# If we're a top-level module containing meta lines but
|
||||
# no valid "require api" line found, complain.
|
||||
self.results.warnings.append(
|
||||
f'Warning: {subpath}:'
|
||||
' no valid "# ba_meta require api <NUM>" line found;'
|
||||
' ignoring module.')
|
||||
' ignoring module.'
|
||||
)
|
||||
return None
|
||||
|
|
|
|||
85
dist/ba_data/python/ba/_multiteamsession.py
vendored
85
dist/ba_data/python/ba/_multiteamsession.py
vendored
|
|
@ -38,6 +38,7 @@ class MultiTeamSession(Session):
|
|||
# pylint: disable=cyclic-import
|
||||
from ba import _playlist
|
||||
from bastd.activity.multiteamjoin import MultiTeamJoinActivity
|
||||
|
||||
app = _ba.app
|
||||
cfg = app.config
|
||||
|
||||
|
|
@ -51,11 +52,13 @@ class MultiTeamSession(Session):
|
|||
# print('FIXME: TEAM BASE SESSION WOULD CALC DEPS.')
|
||||
depsets: Sequence[ba.DependencySet] = []
|
||||
|
||||
super().__init__(depsets,
|
||||
team_names=team_names,
|
||||
team_colors=team_colors,
|
||||
min_players=1,
|
||||
max_players=self.get_max_players())
|
||||
super().__init__(
|
||||
depsets,
|
||||
team_names=team_names,
|
||||
team_colors=team_colors,
|
||||
min_players=1,
|
||||
max_players=self.get_max_players(),
|
||||
)
|
||||
|
||||
self._series_length = app.teams_series_length
|
||||
self._ffa_series_length = app.ffa_series_length
|
||||
|
|
@ -67,13 +70,13 @@ class MultiTeamSession(Session):
|
|||
from bastd.tutorial import TutorialActivity
|
||||
|
||||
# Get this loading.
|
||||
self._tutorial_activity_instance = _ba.newactivity(
|
||||
TutorialActivity)
|
||||
self._tutorial_activity_instance = _ba.newactivity(TutorialActivity)
|
||||
else:
|
||||
self._tutorial_activity_instance = None
|
||||
|
||||
self._playlist_name = cfg.get(self._playlist_selection_var,
|
||||
'__default__')
|
||||
self._playlist_name = cfg.get(
|
||||
self._playlist_selection_var, '__default__'
|
||||
)
|
||||
self._playlist_randomize = cfg.get(self._playlist_randomize_var, False)
|
||||
|
||||
# Which game activity we're on.
|
||||
|
|
@ -81,8 +84,10 @@ class MultiTeamSession(Session):
|
|||
|
||||
playlists = cfg.get(self._playlists_var, {})
|
||||
|
||||
if (self._playlist_name != '__default__'
|
||||
and self._playlist_name in playlists):
|
||||
if (
|
||||
self._playlist_name != '__default__'
|
||||
and self._playlist_name in playlists
|
||||
):
|
||||
|
||||
# Make sure to copy this, as we muck with it in place once we've
|
||||
# got it and we don't want that to affect our config.
|
||||
|
|
@ -98,19 +103,22 @@ class MultiTeamSession(Session):
|
|||
playlist,
|
||||
sessiontype=type(self),
|
||||
add_resolved_type=True,
|
||||
name='default teams' if self.use_teams else 'default ffa')
|
||||
name='default teams' if self.use_teams else 'default ffa',
|
||||
)
|
||||
|
||||
if not playlist_resolved:
|
||||
raise RuntimeError('Playlist contains no valid games.')
|
||||
|
||||
self._playlist = ShuffleList(playlist_resolved,
|
||||
shuffle=self._playlist_randomize)
|
||||
self._playlist = ShuffleList(
|
||||
playlist_resolved, shuffle=self._playlist_randomize
|
||||
)
|
||||
|
||||
# Get a game on deck ready to go.
|
||||
self._current_game_spec: dict[str, Any] | None = None
|
||||
self._next_game_spec: dict[str, Any] = self._playlist.pull_next()
|
||||
self._next_game: type[ba.GameActivity] = (
|
||||
self._next_game_spec['resolved_type'])
|
||||
self._next_game: type[ba.GameActivity] = self._next_game_spec[
|
||||
'resolved_type'
|
||||
]
|
||||
|
||||
# Go ahead and instantiate the next game we'll
|
||||
# use so it has lots of time to load.
|
||||
|
|
@ -131,6 +139,7 @@ class MultiTeamSession(Session):
|
|||
"""Returns a description of the next game on deck."""
|
||||
# pylint: disable=cyclic-import
|
||||
from ba._gameactivity import GameActivity
|
||||
|
||||
gametype: type[GameActivity] = self._next_game_spec['resolved_type']
|
||||
assert issubclass(gametype, GameActivity)
|
||||
return gametype.get_settings_display_string(self._next_game_spec)
|
||||
|
|
@ -151,15 +160,20 @@ class MultiTeamSession(Session):
|
|||
def _instantiate_next_game(self) -> None:
|
||||
self._next_game_instance = _ba.newactivity(
|
||||
self._next_game_spec['resolved_type'],
|
||||
self._next_game_spec['settings'])
|
||||
self._next_game_spec['settings'],
|
||||
)
|
||||
|
||||
def on_activity_end(self, activity: ba.Activity, results: Any) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.tutorial import TutorialActivity
|
||||
from bastd.activity.multiteamvictory import (
|
||||
TeamSeriesVictoryScoreScreenActivity)
|
||||
from ba._activitytypes import (TransitionActivity, JoinActivity,
|
||||
ScoreScreenActivity)
|
||||
TeamSeriesVictoryScoreScreenActivity,
|
||||
)
|
||||
from ba._activitytypes import (
|
||||
TransitionActivity,
|
||||
JoinActivity,
|
||||
ScoreScreenActivity,
|
||||
)
|
||||
|
||||
# If we have a tutorial to show, that's the first thing we do no
|
||||
# matter what.
|
||||
|
|
@ -176,8 +190,8 @@ class MultiTeamSession(Session):
|
|||
# If we're in a between-round activity or a restart-activity, hop
|
||||
# into a round.
|
||||
elif isinstance(
|
||||
activity,
|
||||
(JoinActivity, TransitionActivity, ScoreScreenActivity)):
|
||||
activity, (JoinActivity, TransitionActivity, ScoreScreenActivity)
|
||||
):
|
||||
|
||||
# If we're coming from a series-end activity, reset scores.
|
||||
if isinstance(activity, TeamSeriesVictoryScoreScreenActivity):
|
||||
|
|
@ -204,7 +218,7 @@ class MultiTeamSession(Session):
|
|||
# ..but only ones who have been placed on a team
|
||||
# (ie: no longer sitting in the lobby).
|
||||
try:
|
||||
has_team = (player.sessionteam is not None)
|
||||
has_team = player.sessionteam is not None
|
||||
except NotFoundError:
|
||||
has_team = False
|
||||
if has_team:
|
||||
|
|
@ -223,11 +237,13 @@ class MultiTeamSession(Session):
|
|||
del results # Unused arg.
|
||||
print_error('this should be overridden')
|
||||
|
||||
def announce_game_results(self,
|
||||
activity: ba.GameActivity,
|
||||
results: ba.GameResults,
|
||||
delay: float,
|
||||
announce_winning_team: bool = True) -> None:
|
||||
def announce_game_results(
|
||||
self,
|
||||
activity: ba.GameActivity,
|
||||
results: ba.GameResults,
|
||||
delay: float,
|
||||
announce_winning_team: bool = True,
|
||||
) -> None:
|
||||
"""Show basic game result at the end of a game.
|
||||
|
||||
(before transitioning to a score screen).
|
||||
|
|
@ -243,6 +259,7 @@ class MultiTeamSession(Session):
|
|||
from ba._language import Lstr
|
||||
from ba._freeforallsession import FreeForAllSession
|
||||
from ba._messages import CelebrateMessage
|
||||
|
||||
_ba.timer(delay, Call(_ba.playsound, _ba.getsound('boxingBell')))
|
||||
|
||||
if announce_winning_team:
|
||||
|
|
@ -261,8 +278,10 @@ class MultiTeamSession(Session):
|
|||
wins_resource = 'winsPlayerText'
|
||||
else:
|
||||
wins_resource = 'winsTeamText'
|
||||
wins_text = Lstr(resource=wins_resource,
|
||||
subs=[('${NAME}', winning_sessionteam.name)])
|
||||
wins_text = Lstr(
|
||||
resource=wins_resource,
|
||||
subs=[('${NAME}', winning_sessionteam.name)],
|
||||
)
|
||||
activity.show_zoom_message(
|
||||
wins_text,
|
||||
scale=0.85,
|
||||
|
|
@ -300,8 +319,10 @@ class ShuffleList:
|
|||
# If the new one is the same map or game-type as the previous,
|
||||
# lets try to keep looking.
|
||||
if len(self.shuffle_list) > 1 and self.last_gotten is not None:
|
||||
if (test_obj['settings']['map'] ==
|
||||
self.last_gotten['settings']['map']):
|
||||
if (
|
||||
test_obj['settings']['map']
|
||||
== self.last_gotten['settings']['map']
|
||||
):
|
||||
continue
|
||||
if test_obj['type'] == self.last_gotten['type']:
|
||||
continue
|
||||
|
|
|
|||
176
dist/ba_data/python/ba/_music.py
vendored
176
dist/ba_data/python/ba/_music.py
vendored
|
|
@ -24,6 +24,7 @@ class MusicType(Enum):
|
|||
'situations'. The actual music played for each type can be overridden
|
||||
by the game or by the user.
|
||||
"""
|
||||
|
||||
MENU = 'Menu'
|
||||
VICTORY = 'Victory'
|
||||
CHAR_SELECT = 'CharSelect'
|
||||
|
|
@ -53,6 +54,7 @@ class MusicPlayMode(Enum):
|
|||
|
||||
Category: **Enums**
|
||||
"""
|
||||
|
||||
REGULAR = 'regular'
|
||||
TEST = 'test'
|
||||
|
||||
|
|
@ -63,6 +65,7 @@ class AssetSoundtrackEntry:
|
|||
|
||||
Category: **App Classes**
|
||||
"""
|
||||
|
||||
assetname: str
|
||||
volume: float = 1.0
|
||||
loop: bool = True
|
||||
|
|
@ -70,50 +73,38 @@ class AssetSoundtrackEntry:
|
|||
|
||||
# What gets played by default for our different music types:
|
||||
ASSET_SOUNDTRACK_ENTRIES: dict[MusicType, AssetSoundtrackEntry] = {
|
||||
MusicType.MENU:
|
||||
AssetSoundtrackEntry('menuMusic'),
|
||||
MusicType.VICTORY:
|
||||
AssetSoundtrackEntry('victoryMusic', volume=1.2, loop=False),
|
||||
MusicType.CHAR_SELECT:
|
||||
AssetSoundtrackEntry('charSelectMusic', volume=0.4),
|
||||
MusicType.RUN_AWAY:
|
||||
AssetSoundtrackEntry('runAwayMusic', volume=1.2),
|
||||
MusicType.ONSLAUGHT:
|
||||
AssetSoundtrackEntry('runAwayMusic', volume=1.2),
|
||||
MusicType.KEEP_AWAY:
|
||||
AssetSoundtrackEntry('runAwayMusic', volume=1.2),
|
||||
MusicType.RACE:
|
||||
AssetSoundtrackEntry('runAwayMusic', volume=1.2),
|
||||
MusicType.EPIC_RACE:
|
||||
AssetSoundtrackEntry('slowEpicMusic', volume=1.2),
|
||||
MusicType.SCORES:
|
||||
AssetSoundtrackEntry('scoresEpicMusic', volume=0.6, loop=False),
|
||||
MusicType.GRAND_ROMP:
|
||||
AssetSoundtrackEntry('grandRompMusic', volume=1.2),
|
||||
MusicType.TO_THE_DEATH:
|
||||
AssetSoundtrackEntry('toTheDeathMusic', volume=1.2),
|
||||
MusicType.CHOSEN_ONE:
|
||||
AssetSoundtrackEntry('survivalMusic', volume=0.8),
|
||||
MusicType.FORWARD_MARCH:
|
||||
AssetSoundtrackEntry('forwardMarchMusic', volume=0.8),
|
||||
MusicType.FLAG_CATCHER:
|
||||
AssetSoundtrackEntry('flagCatcherMusic', volume=1.2),
|
||||
MusicType.SURVIVAL:
|
||||
AssetSoundtrackEntry('survivalMusic', volume=0.8),
|
||||
MusicType.EPIC:
|
||||
AssetSoundtrackEntry('slowEpicMusic', volume=1.2),
|
||||
MusicType.SPORTS:
|
||||
AssetSoundtrackEntry('sportsMusic', volume=0.8),
|
||||
MusicType.HOCKEY:
|
||||
AssetSoundtrackEntry('sportsMusic', volume=0.8),
|
||||
MusicType.FOOTBALL:
|
||||
AssetSoundtrackEntry('sportsMusic', volume=0.8),
|
||||
MusicType.FLYING:
|
||||
AssetSoundtrackEntry('flyingMusic', volume=0.8),
|
||||
MusicType.SCARY:
|
||||
AssetSoundtrackEntry('scaryMusic', volume=0.8),
|
||||
MusicType.MARCHING:
|
||||
AssetSoundtrackEntry('whenJohnnyComesMarchingHomeMusic', volume=0.8),
|
||||
MusicType.MENU: AssetSoundtrackEntry('menuMusic'),
|
||||
MusicType.VICTORY: AssetSoundtrackEntry(
|
||||
'victoryMusic', volume=1.2, loop=False
|
||||
),
|
||||
MusicType.CHAR_SELECT: AssetSoundtrackEntry('charSelectMusic', volume=0.4),
|
||||
MusicType.RUN_AWAY: AssetSoundtrackEntry('runAwayMusic', volume=1.2),
|
||||
MusicType.ONSLAUGHT: AssetSoundtrackEntry('runAwayMusic', volume=1.2),
|
||||
MusicType.KEEP_AWAY: AssetSoundtrackEntry('runAwayMusic', volume=1.2),
|
||||
MusicType.RACE: AssetSoundtrackEntry('runAwayMusic', volume=1.2),
|
||||
MusicType.EPIC_RACE: AssetSoundtrackEntry('slowEpicMusic', volume=1.2),
|
||||
MusicType.SCORES: AssetSoundtrackEntry(
|
||||
'scoresEpicMusic', volume=0.6, loop=False
|
||||
),
|
||||
MusicType.GRAND_ROMP: AssetSoundtrackEntry('grandRompMusic', volume=1.2),
|
||||
MusicType.TO_THE_DEATH: AssetSoundtrackEntry('toTheDeathMusic', volume=1.2),
|
||||
MusicType.CHOSEN_ONE: AssetSoundtrackEntry('survivalMusic', volume=0.8),
|
||||
MusicType.FORWARD_MARCH: AssetSoundtrackEntry(
|
||||
'forwardMarchMusic', volume=0.8
|
||||
),
|
||||
MusicType.FLAG_CATCHER: AssetSoundtrackEntry(
|
||||
'flagCatcherMusic', volume=1.2
|
||||
),
|
||||
MusicType.SURVIVAL: AssetSoundtrackEntry('survivalMusic', volume=0.8),
|
||||
MusicType.EPIC: AssetSoundtrackEntry('slowEpicMusic', volume=1.2),
|
||||
MusicType.SPORTS: AssetSoundtrackEntry('sportsMusic', volume=0.8),
|
||||
MusicType.HOCKEY: AssetSoundtrackEntry('sportsMusic', volume=0.8),
|
||||
MusicType.FOOTBALL: AssetSoundtrackEntry('sportsMusic', volume=0.8),
|
||||
MusicType.FLYING: AssetSoundtrackEntry('flyingMusic', volume=0.8),
|
||||
MusicType.SCARY: AssetSoundtrackEntry('scaryMusic', volume=0.8),
|
||||
MusicType.MARCHING: AssetSoundtrackEntry(
|
||||
'whenJohnnyComesMarchingHomeMusic', volume=0.8
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -133,7 +124,7 @@ class MusicSubsystem:
|
|||
self._music_player_type: type[MusicPlayer] | None = None
|
||||
self.music_types: dict[MusicPlayMode, MusicType | None] = {
|
||||
MusicPlayMode.REGULAR: None,
|
||||
MusicPlayMode.TEST: None
|
||||
MusicPlayMode.TEST: None,
|
||||
}
|
||||
|
||||
# Set up custom music players for platforms that support them.
|
||||
|
|
@ -143,9 +134,11 @@ class MusicSubsystem:
|
|||
# instead of a special case.
|
||||
if self.supports_soundtrack_entry_type('musicFile'):
|
||||
from ba.osmusic import OSMusicPlayer
|
||||
|
||||
self._music_player_type = OSMusicPlayer
|
||||
elif self.supports_soundtrack_entry_type('iTunesPlaylist'):
|
||||
from ba.macmusicapp import MacMusicAppMusicPlayer
|
||||
|
||||
self._music_player_type = MacMusicAppMusicPlayer
|
||||
|
||||
def on_app_launch(self) -> None:
|
||||
|
|
@ -156,11 +149,14 @@ class MusicSubsystem:
|
|||
# out than later).
|
||||
try:
|
||||
cfg = _ba.app.config
|
||||
if ('Soundtrack' in cfg and cfg['Soundtrack']
|
||||
not in ['__default__', 'Default Soundtrack']):
|
||||
if 'Soundtrack' in cfg and cfg['Soundtrack'] not in [
|
||||
'__default__',
|
||||
'Default Soundtrack',
|
||||
]:
|
||||
self.get_music_player()
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception('error prepping music-player')
|
||||
|
||||
def on_app_shutdown(self) -> None:
|
||||
|
|
@ -185,9 +181,9 @@ class MusicSubsystem:
|
|||
if self._music_player is not None:
|
||||
self._music_player.set_volume(val)
|
||||
|
||||
def set_music_play_mode(self,
|
||||
mode: MusicPlayMode,
|
||||
force_restart: bool = False) -> None:
|
||||
def set_music_play_mode(
|
||||
self, mode: MusicPlayMode, force_restart: bool = False
|
||||
) -> None:
|
||||
"""Sets music play mode; used for soundtrack testing/etc."""
|
||||
old_mode = self._music_mode
|
||||
self._music_mode = mode
|
||||
|
|
@ -210,8 +206,10 @@ class MusicSubsystem:
|
|||
if entry_type == 'iTunesPlaylist':
|
||||
return 'Mac' in uas
|
||||
if entry_type in ('musicFile', 'musicFolder'):
|
||||
return ('android' in uas
|
||||
and _ba.android_get_external_files_dir() is not None)
|
||||
return (
|
||||
'android' in uas
|
||||
and _ba.android_get_external_files_dir() is not None
|
||||
)
|
||||
if entry_type == 'default':
|
||||
return True
|
||||
return False
|
||||
|
|
@ -228,18 +226,28 @@ class MusicSubsystem:
|
|||
entry_type = 'iTunesPlaylist'
|
||||
|
||||
# For other entries we expect type and name strings in a dict.
|
||||
elif (isinstance(entry, dict) and 'type' in entry
|
||||
and isinstance(entry['type'], str) and 'name' in entry
|
||||
and isinstance(entry['name'], str)):
|
||||
elif (
|
||||
isinstance(entry, dict)
|
||||
and 'type' in entry
|
||||
and isinstance(entry['type'], str)
|
||||
and 'name' in entry
|
||||
and isinstance(entry['name'], str)
|
||||
):
|
||||
entry_type = entry['type']
|
||||
else:
|
||||
raise TypeError('invalid soundtrack entry: ' + str(entry) +
|
||||
' (type ' + str(type(entry)) + ')')
|
||||
raise TypeError(
|
||||
'invalid soundtrack entry: '
|
||||
+ str(entry)
|
||||
+ ' (type '
|
||||
+ str(type(entry))
|
||||
+ ')'
|
||||
)
|
||||
if self.supports_soundtrack_entry_type(entry_type):
|
||||
return entry_type
|
||||
raise ValueError('invalid soundtrack entry:' + str(entry))
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception()
|
||||
return 'default'
|
||||
|
||||
|
|
@ -254,13 +262,18 @@ class MusicSubsystem:
|
|||
return entry
|
||||
|
||||
# For other entries we expect type and name strings in a dict.
|
||||
if (isinstance(entry, dict) and 'type' in entry
|
||||
and isinstance(entry['type'], str) and 'name' in entry
|
||||
and isinstance(entry['name'], str)):
|
||||
if (
|
||||
isinstance(entry, dict)
|
||||
and 'type' in entry
|
||||
and isinstance(entry['type'], str)
|
||||
and 'name' in entry
|
||||
and isinstance(entry['name'], str)
|
||||
):
|
||||
return entry['name']
|
||||
raise ValueError('invalid soundtrack entry:' + str(entry))
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception()
|
||||
return 'default'
|
||||
|
||||
|
|
@ -269,11 +282,13 @@ class MusicSubsystem:
|
|||
if _ba.is_os_playing_music():
|
||||
self.do_play_music(None)
|
||||
|
||||
def do_play_music(self,
|
||||
musictype: MusicType | str | None,
|
||||
continuous: bool = False,
|
||||
mode: MusicPlayMode = MusicPlayMode.REGULAR,
|
||||
testsoundtrack: dict[str, Any] | None = None) -> None:
|
||||
def do_play_music(
|
||||
self,
|
||||
musictype: MusicType | str | None,
|
||||
continuous: bool = False,
|
||||
mode: MusicPlayMode = MusicPlayMode.REGULAR,
|
||||
testsoundtrack: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Plays the requested music type/mode.
|
||||
|
||||
For most cases, setmusic() is the proper call to use, which itself
|
||||
|
|
@ -378,8 +393,9 @@ class MusicSubsystem:
|
|||
'positional': False,
|
||||
'music': True,
|
||||
'volume': entry.volume * 5.0,
|
||||
'loop': entry.loop
|
||||
})
|
||||
'loop': entry.loop,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class MusicPlayer:
|
||||
|
|
@ -397,11 +413,16 @@ class MusicPlayer:
|
|||
self._volume = 1.0
|
||||
self._actually_playing = False
|
||||
|
||||
def select_entry(self, callback: Callable[[Any], None], current_entry: Any,
|
||||
selection_target_name: str) -> Any:
|
||||
def select_entry(
|
||||
self,
|
||||
callback: Callable[[Any], None],
|
||||
current_entry: Any,
|
||||
selection_target_name: str,
|
||||
) -> Any:
|
||||
"""Summons a UI to select a new soundtrack entry."""
|
||||
return self.on_select_entry(callback, current_entry,
|
||||
selection_target_name)
|
||||
return self.on_select_entry(
|
||||
callback, current_entry, selection_target_name
|
||||
)
|
||||
|
||||
def set_volume(self, volume: float) -> None:
|
||||
"""Set player volume (value should be between 0 and 1)."""
|
||||
|
|
@ -435,8 +456,12 @@ class MusicPlayer:
|
|||
"""Shutdown music playback completely."""
|
||||
self.on_app_shutdown()
|
||||
|
||||
def on_select_entry(self, callback: Callable[[Any], None],
|
||||
current_entry: Any, selection_target_name: str) -> Any:
|
||||
def on_select_entry(
|
||||
self,
|
||||
callback: Callable[[Any], None],
|
||||
current_entry: Any,
|
||||
selection_target_name: str,
|
||||
) -> Any:
|
||||
"""Present a GUI to select an entry.
|
||||
|
||||
The callback should be called with a valid entry or None to
|
||||
|
|
@ -464,8 +489,9 @@ class MusicPlayer:
|
|||
self.on_play(self._entry_to_play)
|
||||
self._actually_playing = True
|
||||
else:
|
||||
if self._actually_playing and (self._entry_to_play is None
|
||||
or self._volume <= 0.0):
|
||||
if self._actually_playing and (
|
||||
self._entry_to_play is None or self._volume <= 0.0
|
||||
):
|
||||
self.on_stop()
|
||||
self._actually_playing = False
|
||||
|
||||
|
|
|
|||
69
dist/ba_data/python/ba/_net.py
vendored
69
dist/ba_data/python/ba/_net.py
vendored
|
|
@ -15,6 +15,7 @@ import _ba
|
|||
if TYPE_CHECKING:
|
||||
from typing import Any, Callable
|
||||
import socket
|
||||
|
||||
MasterServerCallback = Callable[[None | dict[str, Any]], None]
|
||||
|
||||
# Timeout for standard functions talking to the master-server/etc.
|
||||
|
|
@ -61,6 +62,7 @@ class NetworkSubsystem:
|
|||
def get_ip_address_type(addr: str) -> socket.AddressFamily:
|
||||
"""Return socket.AF_INET6 or socket.AF_INET4 for the provided address."""
|
||||
import socket
|
||||
|
||||
socket_type = None
|
||||
|
||||
# First try it as an ipv4 address.
|
||||
|
|
@ -84,16 +86,21 @@ def get_ip_address_type(addr: str) -> socket.AddressFamily:
|
|||
|
||||
class MasterServerResponseType(Enum):
|
||||
"""How to interpret responses from the master-server."""
|
||||
|
||||
JSON = 0
|
||||
|
||||
|
||||
class MasterServerCallThread(threading.Thread):
|
||||
"""Thread to communicate with the master-server."""
|
||||
|
||||
def __init__(self, request: str, request_type: str,
|
||||
data: dict[str, Any] | None,
|
||||
callback: MasterServerCallback | None,
|
||||
response_type: MasterServerResponseType):
|
||||
def __init__(
|
||||
self,
|
||||
request: str,
|
||||
request_type: str,
|
||||
data: dict[str, Any] | None,
|
||||
callback: MasterServerCallback | None,
|
||||
response_type: MasterServerResponseType,
|
||||
):
|
||||
super().__init__()
|
||||
self._request = request
|
||||
self._request_type = request_type
|
||||
|
|
@ -106,8 +113,7 @@ class MasterServerCallThread(threading.Thread):
|
|||
|
||||
# Save and restore the context we were created from.
|
||||
activity = _ba.getactivity(doraise=False)
|
||||
self._activity = weakref.ref(
|
||||
activity) if activity is not None else None
|
||||
self._activity = weakref.ref(activity) if activity is not None else None
|
||||
|
||||
def _run_callback(self, arg: None | dict[str, Any]) -> None:
|
||||
# If we were created in an activity context and that activity has
|
||||
|
|
@ -143,22 +149,31 @@ class MasterServerCallThread(threading.Thread):
|
|||
self._data = utf8_all(self._data)
|
||||
_ba.set_thread_name('BA_ServerCallThread')
|
||||
if self._request_type == 'get':
|
||||
url = (get_master_server_address() + '/' + self._request +
|
||||
'?' + urllib.parse.urlencode(self._data))
|
||||
url = (
|
||||
get_master_server_address()
|
||||
+ '/'
|
||||
+ self._request
|
||||
+ '?'
|
||||
+ urllib.parse.urlencode(self._data)
|
||||
)
|
||||
response = urllib.request.urlopen(
|
||||
urllib.request.Request(
|
||||
url, None, {'User-Agent': _ba.app.user_agent_string}),
|
||||
url, None, {'User-Agent': _ba.app.user_agent_string}
|
||||
),
|
||||
context=_ba.app.net.sslcontext,
|
||||
timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS)
|
||||
timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS,
|
||||
)
|
||||
elif self._request_type == 'post':
|
||||
url = get_master_server_address() + '/' + self._request
|
||||
response = urllib.request.urlopen(
|
||||
urllib.request.Request(
|
||||
url,
|
||||
urllib.parse.urlencode(self._data).encode(),
|
||||
{'User-Agent': _ba.app.user_agent_string}),
|
||||
{'User-Agent': _ba.app.user_agent_string},
|
||||
),
|
||||
context=_ba.app.net.sslcontext,
|
||||
timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS)
|
||||
timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS,
|
||||
)
|
||||
else:
|
||||
raise TypeError('Invalid request_type: ' + self._request_type)
|
||||
|
||||
|
|
@ -180,37 +195,43 @@ class MasterServerCallThread(threading.Thread):
|
|||
|
||||
# Ignore common network errors; note unexpected ones.
|
||||
if not is_urllib_communication_error(exc, url=url):
|
||||
print(f'Error in MasterServerCallThread'
|
||||
f' (url={url},'
|
||||
f' response-type={self._response_type},'
|
||||
f' response-data={response_data}):')
|
||||
print(
|
||||
f'Error in MasterServerCallThread'
|
||||
f' (url={url},'
|
||||
f' response-type={self._response_type},'
|
||||
f' response-data={response_data}):'
|
||||
)
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
response_data = None
|
||||
|
||||
if self._callback is not None:
|
||||
_ba.pushcall(Call(self._run_callback, response_data),
|
||||
from_other_thread=True)
|
||||
_ba.pushcall(
|
||||
Call(self._run_callback, response_data), from_other_thread=True
|
||||
)
|
||||
|
||||
|
||||
def master_server_get(
|
||||
request: str,
|
||||
data: dict[str, Any],
|
||||
callback: MasterServerCallback | None = None,
|
||||
response_type: MasterServerResponseType = MasterServerResponseType.JSON
|
||||
response_type: MasterServerResponseType = MasterServerResponseType.JSON,
|
||||
) -> None:
|
||||
"""Make a call to the master server via a http GET."""
|
||||
MasterServerCallThread(request, 'get', data, callback,
|
||||
response_type).start()
|
||||
MasterServerCallThread(
|
||||
request, 'get', data, callback, response_type
|
||||
).start()
|
||||
|
||||
|
||||
def master_server_post(
|
||||
request: str,
|
||||
data: dict[str, Any],
|
||||
callback: MasterServerCallback | None = None,
|
||||
response_type: MasterServerResponseType = MasterServerResponseType.JSON
|
||||
response_type: MasterServerResponseType = MasterServerResponseType.JSON,
|
||||
) -> None:
|
||||
"""Make a call to the master server via a http POST."""
|
||||
MasterServerCallThread(request, 'post', data, callback,
|
||||
response_type).start()
|
||||
MasterServerCallThread(
|
||||
request, 'post', data, callback, response_type
|
||||
).start()
|
||||
|
|
|
|||
22
dist/ba_data/python/ba/_player.py
vendored
22
dist/ba_data/python/ba/_player.py
vendored
|
|
@ -8,8 +8,11 @@ from dataclasses import dataclass
|
|||
from typing import TYPE_CHECKING, TypeVar, Generic, cast
|
||||
|
||||
import _ba
|
||||
from ba._error import (SessionPlayerNotFoundError, print_exception,
|
||||
ActorNotFoundError)
|
||||
from ba._error import (
|
||||
SessionPlayerNotFoundError,
|
||||
print_exception,
|
||||
ActorNotFoundError,
|
||||
)
|
||||
from ba._messages import DeathType, DieMessage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -28,6 +31,7 @@ class PlayerInfo:
|
|||
|
||||
Category: Gameplay Classes
|
||||
"""
|
||||
|
||||
name: str
|
||||
character: str
|
||||
|
||||
|
|
@ -38,6 +42,7 @@ class StandLocation:
|
|||
|
||||
Category: Gameplay Classes
|
||||
"""
|
||||
|
||||
position: ba.Vec3
|
||||
angle: float | None = None
|
||||
|
||||
|
|
@ -90,7 +95,8 @@ class Player(Generic[TeamType]):
|
|||
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.')
|
||||
f' in the class decorator.'
|
||||
)
|
||||
|
||||
self.actor = None
|
||||
self.character = ''
|
||||
|
|
@ -249,8 +255,9 @@ class Player(Generic[TeamType]):
|
|||
assert not self._expired
|
||||
return self._sessionplayer.get_icon()
|
||||
|
||||
def assigninput(self, inputtype: ba.InputType | tuple[ba.InputType, ...],
|
||||
call: Callable) -> None:
|
||||
def assigninput(
|
||||
self, inputtype: ba.InputType | tuple[ba.InputType, ...], call: Callable
|
||||
) -> None:
|
||||
"""
|
||||
Set the python callable to be run for one or more types of input.
|
||||
"""
|
||||
|
|
@ -311,8 +318,9 @@ def playercast(totype: type[PlayerType], player: ba.Player) -> PlayerType:
|
|||
# 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: ba.Player | None) -> PlayerType | None:
|
||||
def playercast_o(
|
||||
totype: type[PlayerType], player: ba.Player | None
|
||||
) -> PlayerType | None:
|
||||
"""A variant of ba.playercast() for use with optional ba.Player values.
|
||||
|
||||
Category: Gameplay Functions
|
||||
|
|
|
|||
759
dist/ba_data/python/ba/_playlist.py
vendored
759
dist/ba_data/python/ba/_playlist.py
vendored
|
|
@ -15,12 +15,14 @@ if TYPE_CHECKING:
|
|||
PlaylistType = list[dict[str, Any]]
|
||||
|
||||
|
||||
def filter_playlist(playlist: PlaylistType,
|
||||
sessiontype: type[_session.Session],
|
||||
add_resolved_type: bool = False,
|
||||
remove_unowned: bool = True,
|
||||
mark_unowned: bool = False,
|
||||
name: str = '?') -> PlaylistType:
|
||||
def filter_playlist(
|
||||
playlist: PlaylistType,
|
||||
sessiontype: type[_session.Session],
|
||||
add_resolved_type: bool = False,
|
||||
remove_unowned: bool = True,
|
||||
mark_unowned: bool = False,
|
||||
name: str = '?',
|
||||
) -> PlaylistType:
|
||||
"""Return a filtered version of a playlist.
|
||||
|
||||
Strips out or replaces invalid or unowned game types, makes sure all
|
||||
|
|
@ -34,6 +36,7 @@ def filter_playlist(playlist: PlaylistType,
|
|||
from ba._store import get_unowned_maps, get_unowned_game_types
|
||||
from ba._general import getclass
|
||||
from ba._gameactivity import GameActivity
|
||||
|
||||
goodlist: list[dict] = []
|
||||
unowned_maps: Sequence[str]
|
||||
if remove_unowned or mark_unowned:
|
||||
|
|
@ -56,7 +59,8 @@ def filter_playlist(playlist: PlaylistType,
|
|||
|
||||
# Update old map names to new ones.
|
||||
entry['settings']['map'] = get_filtered_map_name(
|
||||
entry['settings']['map'])
|
||||
entry['settings']['map']
|
||||
)
|
||||
if remove_unowned and entry['settings']['map'] in unowned_maps:
|
||||
continue
|
||||
|
||||
|
|
@ -67,60 +71,89 @@ def filter_playlist(playlist: PlaylistType,
|
|||
raise TypeError('invalid entry format')
|
||||
try:
|
||||
# Do some type filters for backwards compat.
|
||||
if entry['type'] in ('Assault.AssaultGame',
|
||||
'Happy_Thoughts.HappyThoughtsGame',
|
||||
'bsAssault.AssaultGame',
|
||||
'bs_assault.AssaultGame'):
|
||||
if entry['type'] in (
|
||||
'Assault.AssaultGame',
|
||||
'Happy_Thoughts.HappyThoughtsGame',
|
||||
'bsAssault.AssaultGame',
|
||||
'bs_assault.AssaultGame',
|
||||
):
|
||||
entry['type'] = 'bastd.game.assault.AssaultGame'
|
||||
if entry['type'] in ('King_of_the_Hill.KingOfTheHillGame',
|
||||
'bsKingOfTheHill.KingOfTheHillGame',
|
||||
'bs_king_of_the_hill.KingOfTheHillGame'):
|
||||
if entry['type'] in (
|
||||
'King_of_the_Hill.KingOfTheHillGame',
|
||||
'bsKingOfTheHill.KingOfTheHillGame',
|
||||
'bs_king_of_the_hill.KingOfTheHillGame',
|
||||
):
|
||||
entry['type'] = 'bastd.game.kingofthehill.KingOfTheHillGame'
|
||||
if entry['type'] in ('Capture_the_Flag.CTFGame',
|
||||
'bsCaptureTheFlag.CTFGame',
|
||||
'bs_capture_the_flag.CTFGame'):
|
||||
entry['type'] = (
|
||||
'bastd.game.capturetheflag.CaptureTheFlagGame')
|
||||
if entry['type'] in ('Death_Match.DeathMatchGame',
|
||||
'bsDeathMatch.DeathMatchGame',
|
||||
'bs_death_match.DeathMatchGame'):
|
||||
if entry['type'] in (
|
||||
'Capture_the_Flag.CTFGame',
|
||||
'bsCaptureTheFlag.CTFGame',
|
||||
'bs_capture_the_flag.CTFGame',
|
||||
):
|
||||
entry['type'] = 'bastd.game.capturetheflag.CaptureTheFlagGame'
|
||||
if entry['type'] in (
|
||||
'Death_Match.DeathMatchGame',
|
||||
'bsDeathMatch.DeathMatchGame',
|
||||
'bs_death_match.DeathMatchGame',
|
||||
):
|
||||
entry['type'] = 'bastd.game.deathmatch.DeathMatchGame'
|
||||
if entry['type'] in ('ChosenOne.ChosenOneGame',
|
||||
'bsChosenOne.ChosenOneGame',
|
||||
'bs_chosen_one.ChosenOneGame'):
|
||||
if entry['type'] in (
|
||||
'ChosenOne.ChosenOneGame',
|
||||
'bsChosenOne.ChosenOneGame',
|
||||
'bs_chosen_one.ChosenOneGame',
|
||||
):
|
||||
entry['type'] = 'bastd.game.chosenone.ChosenOneGame'
|
||||
if entry['type'] in ('Conquest.Conquest', 'Conquest.ConquestGame',
|
||||
'bsConquest.ConquestGame',
|
||||
'bs_conquest.ConquestGame'):
|
||||
if entry['type'] in (
|
||||
'Conquest.Conquest',
|
||||
'Conquest.ConquestGame',
|
||||
'bsConquest.ConquestGame',
|
||||
'bs_conquest.ConquestGame',
|
||||
):
|
||||
entry['type'] = 'bastd.game.conquest.ConquestGame'
|
||||
if entry['type'] in ('Elimination.EliminationGame',
|
||||
'bsElimination.EliminationGame',
|
||||
'bs_elimination.EliminationGame'):
|
||||
if entry['type'] in (
|
||||
'Elimination.EliminationGame',
|
||||
'bsElimination.EliminationGame',
|
||||
'bs_elimination.EliminationGame',
|
||||
):
|
||||
entry['type'] = 'bastd.game.elimination.EliminationGame'
|
||||
if entry['type'] in ('Football.FootballGame',
|
||||
'bsFootball.FootballTeamGame',
|
||||
'bs_football.FootballTeamGame'):
|
||||
if entry['type'] in (
|
||||
'Football.FootballGame',
|
||||
'bsFootball.FootballTeamGame',
|
||||
'bs_football.FootballTeamGame',
|
||||
):
|
||||
entry['type'] = 'bastd.game.football.FootballTeamGame'
|
||||
if entry['type'] in ('Hockey.HockeyGame', 'bsHockey.HockeyGame',
|
||||
'bs_hockey.HockeyGame'):
|
||||
if entry['type'] in (
|
||||
'Hockey.HockeyGame',
|
||||
'bsHockey.HockeyGame',
|
||||
'bs_hockey.HockeyGame',
|
||||
):
|
||||
entry['type'] = 'bastd.game.hockey.HockeyGame'
|
||||
if entry['type'] in ('Keep_Away.KeepAwayGame',
|
||||
'bsKeepAway.KeepAwayGame',
|
||||
'bs_keep_away.KeepAwayGame'):
|
||||
if entry['type'] in (
|
||||
'Keep_Away.KeepAwayGame',
|
||||
'bsKeepAway.KeepAwayGame',
|
||||
'bs_keep_away.KeepAwayGame',
|
||||
):
|
||||
entry['type'] = 'bastd.game.keepaway.KeepAwayGame'
|
||||
if entry['type'] in ('Race.RaceGame', 'bsRace.RaceGame',
|
||||
'bs_race.RaceGame'):
|
||||
if entry['type'] in (
|
||||
'Race.RaceGame',
|
||||
'bsRace.RaceGame',
|
||||
'bs_race.RaceGame',
|
||||
):
|
||||
entry['type'] = 'bastd.game.race.RaceGame'
|
||||
if entry['type'] in ('bsEasterEggHunt.EasterEggHuntGame',
|
||||
'bs_easter_egg_hunt.EasterEggHuntGame'):
|
||||
if entry['type'] in (
|
||||
'bsEasterEggHunt.EasterEggHuntGame',
|
||||
'bs_easter_egg_hunt.EasterEggHuntGame',
|
||||
):
|
||||
entry['type'] = 'bastd.game.easteregghunt.EasterEggHuntGame'
|
||||
if entry['type'] in ('bsMeteorShower.MeteorShowerGame',
|
||||
'bs_meteor_shower.MeteorShowerGame'):
|
||||
if entry['type'] in (
|
||||
'bsMeteorShower.MeteorShowerGame',
|
||||
'bs_meteor_shower.MeteorShowerGame',
|
||||
):
|
||||
entry['type'] = 'bastd.game.meteorshower.MeteorShowerGame'
|
||||
if entry['type'] in ('bsTargetPractice.TargetPracticeGame',
|
||||
'bs_target_practice.TargetPracticeGame'):
|
||||
entry['type'] = (
|
||||
'bastd.game.targetpractice.TargetPracticeGame')
|
||||
if entry['type'] in (
|
||||
'bsTargetPractice.TargetPracticeGame',
|
||||
'bs_target_practice.TargetPracticeGame',
|
||||
):
|
||||
entry['type'] = 'bastd.game.targetpractice.TargetPracticeGame'
|
||||
|
||||
gameclass = getclass(entry['type'], GameActivity)
|
||||
|
||||
|
|
@ -140,10 +173,12 @@ def filter_playlist(playlist: PlaylistType,
|
|||
entry['settings'][setting.name] = setting.default
|
||||
goodlist.append(entry)
|
||||
except ImportError as exc:
|
||||
logging.warning('Import failed while scanning playlist \'%s\': %s',
|
||||
name, exc)
|
||||
logging.warning(
|
||||
'Import failed while scanning playlist \'%s\': %s', name, exc
|
||||
)
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception()
|
||||
return goodlist
|
||||
|
||||
|
|
@ -155,123 +190,134 @@ def get_default_free_for_all_playlist() -> PlaylistType:
|
|||
# but filtering translates them properly to the new ones.
|
||||
# (is kinda a handy way to ensure filtering is working).
|
||||
# Eventually should update these though.
|
||||
return [{
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'Kills to Win Per Player': 10,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 300,
|
||||
'map': 'Doom Shroom'
|
||||
return [
|
||||
{
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'Kills to Win Per Player': 10,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 300,
|
||||
'map': 'Doom Shroom',
|
||||
},
|
||||
'type': 'bs_death_match.DeathMatchGame',
|
||||
},
|
||||
'type': 'bs_death_match.DeathMatchGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Chosen One Gets Gloves': True,
|
||||
'Chosen One Gets Shield': False,
|
||||
'Chosen One Time': 30,
|
||||
'Epic Mode': 0,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 300,
|
||||
'map': 'Monkey Face'
|
||||
{
|
||||
'settings': {
|
||||
'Chosen One Gets Gloves': True,
|
||||
'Chosen One Gets Shield': False,
|
||||
'Chosen One Time': 30,
|
||||
'Epic Mode': 0,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 300,
|
||||
'map': 'Monkey Face',
|
||||
},
|
||||
'type': 'bs_chosen_one.ChosenOneGame',
|
||||
},
|
||||
'type': 'bs_chosen_one.ChosenOneGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Hold Time': 30,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 300,
|
||||
'map': 'Zigzag'
|
||||
{
|
||||
'settings': {
|
||||
'Hold Time': 30,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 300,
|
||||
'map': 'Zigzag',
|
||||
},
|
||||
'type': 'bs_king_of_the_hill.KingOfTheHillGame',
|
||||
},
|
||||
'type': 'bs_king_of_the_hill.KingOfTheHillGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'map': 'Rampage'
|
||||
{
|
||||
'settings': {'Epic Mode': False, 'map': 'Rampage'},
|
||||
'type': 'bs_meteor_shower.MeteorShowerGame',
|
||||
},
|
||||
'type': 'bs_meteor_shower.MeteorShowerGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Epic Mode': 1,
|
||||
'Lives Per Player': 1,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 120,
|
||||
'map': 'Tip Top'
|
||||
{
|
||||
'settings': {
|
||||
'Epic Mode': 1,
|
||||
'Lives Per Player': 1,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 120,
|
||||
'map': 'Tip Top',
|
||||
},
|
||||
'type': 'bs_elimination.EliminationGame',
|
||||
},
|
||||
'type': 'bs_elimination.EliminationGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Hold Time': 30,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 300,
|
||||
'map': 'The Pad'
|
||||
{
|
||||
'settings': {
|
||||
'Hold Time': 30,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 300,
|
||||
'map': 'The Pad',
|
||||
},
|
||||
'type': 'bs_keep_away.KeepAwayGame',
|
||||
},
|
||||
'type': 'bs_keep_away.KeepAwayGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Epic Mode': True,
|
||||
'Kills to Win Per Player': 10,
|
||||
'Respawn Times': 0.25,
|
||||
'Time Limit': 120,
|
||||
'map': 'Rampage'
|
||||
{
|
||||
'settings': {
|
||||
'Epic Mode': True,
|
||||
'Kills to Win Per Player': 10,
|
||||
'Respawn Times': 0.25,
|
||||
'Time Limit': 120,
|
||||
'map': 'Rampage',
|
||||
},
|
||||
'type': 'bs_death_match.DeathMatchGame',
|
||||
},
|
||||
'type': 'bs_death_match.DeathMatchGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Bomb Spawning': 1000,
|
||||
'Epic Mode': False,
|
||||
'Laps': 3,
|
||||
'Mine Spawn Interval': 4000,
|
||||
'Mine Spawning': 4000,
|
||||
'Time Limit': 300,
|
||||
'map': 'Big G'
|
||||
{
|
||||
'settings': {
|
||||
'Bomb Spawning': 1000,
|
||||
'Epic Mode': False,
|
||||
'Laps': 3,
|
||||
'Mine Spawn Interval': 4000,
|
||||
'Mine Spawning': 4000,
|
||||
'Time Limit': 300,
|
||||
'map': 'Big G',
|
||||
},
|
||||
'type': 'bs_race.RaceGame',
|
||||
},
|
||||
'type': 'bs_race.RaceGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Hold Time': 30,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 300,
|
||||
'map': 'Happy Thoughts'
|
||||
{
|
||||
'settings': {
|
||||
'Hold Time': 30,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 300,
|
||||
'map': 'Happy Thoughts',
|
||||
},
|
||||
'type': 'bs_king_of_the_hill.KingOfTheHillGame',
|
||||
},
|
||||
'type': 'bs_king_of_the_hill.KingOfTheHillGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Enable Impact Bombs': 1,
|
||||
'Enable Triple Bombs': False,
|
||||
'Target Count': 2,
|
||||
'map': 'Doom Shroom'
|
||||
{
|
||||
'settings': {
|
||||
'Enable Impact Bombs': 1,
|
||||
'Enable Triple Bombs': False,
|
||||
'Target Count': 2,
|
||||
'map': 'Doom Shroom',
|
||||
},
|
||||
'type': 'bs_target_practice.TargetPracticeGame',
|
||||
},
|
||||
'type': 'bs_target_practice.TargetPracticeGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'Lives Per Player': 5,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 300,
|
||||
'map': 'Step Right Up'
|
||||
{
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'Lives Per Player': 5,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 300,
|
||||
'map': 'Step Right Up',
|
||||
},
|
||||
'type': 'bs_elimination.EliminationGame',
|
||||
},
|
||||
'type': 'bs_elimination.EliminationGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'Kills to Win Per Player': 10,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 300,
|
||||
'map': 'Crag Castle'
|
||||
{
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'Kills to Win Per Player': 10,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 300,
|
||||
'map': 'Crag Castle',
|
||||
},
|
||||
'type': 'bs_death_match.DeathMatchGame',
|
||||
},
|
||||
'type': 'bs_death_match.DeathMatchGame'
|
||||
}, {
|
||||
'map': 'Lake Frigid',
|
||||
'settings': {
|
||||
'Bomb Spawning': 0,
|
||||
'Epic Mode': False,
|
||||
'Laps': 6,
|
||||
'Mine Spawning': 2000,
|
||||
'Time Limit': 300,
|
||||
'map': 'Lake Frigid'
|
||||
{
|
||||
'map': 'Lake Frigid',
|
||||
'settings': {
|
||||
'Bomb Spawning': 0,
|
||||
'Epic Mode': False,
|
||||
'Laps': 6,
|
||||
'Mine Spawning': 2000,
|
||||
'Time Limit': 300,
|
||||
'map': 'Lake Frigid',
|
||||
},
|
||||
'type': 'bs_race.RaceGame',
|
||||
},
|
||||
'type': 'bs_race.RaceGame'
|
||||
}]
|
||||
]
|
||||
|
||||
|
||||
def get_default_teams_playlist() -> PlaylistType:
|
||||
|
|
@ -281,217 +327,238 @@ def get_default_teams_playlist() -> PlaylistType:
|
|||
# but filtering translates them properly to the new ones.
|
||||
# (is kinda a handy way to ensure filtering is working).
|
||||
# Eventually should update these though.
|
||||
return [{
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'Flag Idle Return Time': 30,
|
||||
'Flag Touch Return Time': 0,
|
||||
'Respawn Times': 1.0,
|
||||
'Score to Win': 3,
|
||||
'Time Limit': 600,
|
||||
'map': 'Bridgit'
|
||||
return [
|
||||
{
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'Flag Idle Return Time': 30,
|
||||
'Flag Touch Return Time': 0,
|
||||
'Respawn Times': 1.0,
|
||||
'Score to Win': 3,
|
||||
'Time Limit': 600,
|
||||
'map': 'Bridgit',
|
||||
},
|
||||
'type': 'bs_capture_the_flag.CTFGame',
|
||||
},
|
||||
'type': 'bs_capture_the_flag.CTFGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'Respawn Times': 1.0,
|
||||
'Score to Win': 3,
|
||||
'Time Limit': 600,
|
||||
'map': 'Step Right Up'
|
||||
{
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'Respawn Times': 1.0,
|
||||
'Score to Win': 3,
|
||||
'Time Limit': 600,
|
||||
'map': 'Step Right Up',
|
||||
},
|
||||
'type': 'bs_assault.AssaultGame',
|
||||
},
|
||||
'type': 'bs_assault.AssaultGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Balance Total Lives': False,
|
||||
'Epic Mode': False,
|
||||
'Lives Per Player': 3,
|
||||
'Respawn Times': 1.0,
|
||||
'Solo Mode': True,
|
||||
'Time Limit': 600,
|
||||
'map': 'Rampage'
|
||||
{
|
||||
'settings': {
|
||||
'Balance Total Lives': False,
|
||||
'Epic Mode': False,
|
||||
'Lives Per Player': 3,
|
||||
'Respawn Times': 1.0,
|
||||
'Solo Mode': True,
|
||||
'Time Limit': 600,
|
||||
'map': 'Rampage',
|
||||
},
|
||||
'type': 'bs_elimination.EliminationGame',
|
||||
},
|
||||
'type': 'bs_elimination.EliminationGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'Kills to Win Per Player': 5,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 300,
|
||||
'map': 'Roundabout'
|
||||
{
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'Kills to Win Per Player': 5,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 300,
|
||||
'map': 'Roundabout',
|
||||
},
|
||||
'type': 'bs_death_match.DeathMatchGame',
|
||||
},
|
||||
'type': 'bs_death_match.DeathMatchGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Respawn Times': 1.0,
|
||||
'Score to Win': 1,
|
||||
'Time Limit': 600,
|
||||
'map': 'Hockey Stadium'
|
||||
{
|
||||
'settings': {
|
||||
'Respawn Times': 1.0,
|
||||
'Score to Win': 1,
|
||||
'Time Limit': 600,
|
||||
'map': 'Hockey Stadium',
|
||||
},
|
||||
'type': 'bs_hockey.HockeyGame',
|
||||
},
|
||||
'type': 'bs_hockey.HockeyGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Hold Time': 30,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 300,
|
||||
'map': 'Monkey Face'
|
||||
{
|
||||
'settings': {
|
||||
'Hold Time': 30,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 300,
|
||||
'map': 'Monkey Face',
|
||||
},
|
||||
'type': 'bs_keep_away.KeepAwayGame',
|
||||
},
|
||||
'type': 'bs_keep_away.KeepAwayGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Balance Total Lives': False,
|
||||
'Epic Mode': True,
|
||||
'Lives Per Player': 1,
|
||||
'Respawn Times': 1.0,
|
||||
'Solo Mode': False,
|
||||
'Time Limit': 120,
|
||||
'map': 'Tip Top'
|
||||
{
|
||||
'settings': {
|
||||
'Balance Total Lives': False,
|
||||
'Epic Mode': True,
|
||||
'Lives Per Player': 1,
|
||||
'Respawn Times': 1.0,
|
||||
'Solo Mode': False,
|
||||
'Time Limit': 120,
|
||||
'map': 'Tip Top',
|
||||
},
|
||||
'type': 'bs_elimination.EliminationGame',
|
||||
},
|
||||
'type': 'bs_elimination.EliminationGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'Respawn Times': 1.0,
|
||||
'Score to Win': 3,
|
||||
'Time Limit': 300,
|
||||
'map': 'Crag Castle'
|
||||
{
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'Respawn Times': 1.0,
|
||||
'Score to Win': 3,
|
||||
'Time Limit': 300,
|
||||
'map': 'Crag Castle',
|
||||
},
|
||||
'type': 'bs_assault.AssaultGame',
|
||||
},
|
||||
'type': 'bs_assault.AssaultGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'Kills to Win Per Player': 5,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 300,
|
||||
'map': 'Doom Shroom'
|
||||
{
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'Kills to Win Per Player': 5,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 300,
|
||||
'map': 'Doom Shroom',
|
||||
},
|
||||
'type': 'bs_death_match.DeathMatchGame',
|
||||
},
|
||||
'type': 'bs_death_match.DeathMatchGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'map': 'Rampage'
|
||||
{
|
||||
'settings': {'Epic Mode': False, 'map': 'Rampage'},
|
||||
'type': 'bs_meteor_shower.MeteorShowerGame',
|
||||
},
|
||||
'type': 'bs_meteor_shower.MeteorShowerGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'Flag Idle Return Time': 30,
|
||||
'Flag Touch Return Time': 0,
|
||||
'Respawn Times': 1.0,
|
||||
'Score to Win': 2,
|
||||
'Time Limit': 600,
|
||||
'map': 'Roundabout'
|
||||
{
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'Flag Idle Return Time': 30,
|
||||
'Flag Touch Return Time': 0,
|
||||
'Respawn Times': 1.0,
|
||||
'Score to Win': 2,
|
||||
'Time Limit': 600,
|
||||
'map': 'Roundabout',
|
||||
},
|
||||
'type': 'bs_capture_the_flag.CTFGame',
|
||||
},
|
||||
'type': 'bs_capture_the_flag.CTFGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Respawn Times': 1.0,
|
||||
'Score to Win': 21,
|
||||
'Time Limit': 600,
|
||||
'map': 'Football Stadium'
|
||||
{
|
||||
'settings': {
|
||||
'Respawn Times': 1.0,
|
||||
'Score to Win': 21,
|
||||
'Time Limit': 600,
|
||||
'map': 'Football Stadium',
|
||||
},
|
||||
'type': 'bs_football.FootballTeamGame',
|
||||
},
|
||||
'type': 'bs_football.FootballTeamGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Epic Mode': True,
|
||||
'Respawn Times': 0.25,
|
||||
'Score to Win': 3,
|
||||
'Time Limit': 120,
|
||||
'map': 'Bridgit'
|
||||
{
|
||||
'settings': {
|
||||
'Epic Mode': True,
|
||||
'Respawn Times': 0.25,
|
||||
'Score to Win': 3,
|
||||
'Time Limit': 120,
|
||||
'map': 'Bridgit',
|
||||
},
|
||||
'type': 'bs_assault.AssaultGame',
|
||||
},
|
||||
'type': 'bs_assault.AssaultGame'
|
||||
}, {
|
||||
'map': 'Doom Shroom',
|
||||
'settings': {
|
||||
'Enable Impact Bombs': 1,
|
||||
'Enable Triple Bombs': False,
|
||||
'Target Count': 2,
|
||||
'map': 'Doom Shroom'
|
||||
{
|
||||
'map': 'Doom Shroom',
|
||||
'settings': {
|
||||
'Enable Impact Bombs': 1,
|
||||
'Enable Triple Bombs': False,
|
||||
'Target Count': 2,
|
||||
'map': 'Doom Shroom',
|
||||
},
|
||||
'type': 'bs_target_practice.TargetPracticeGame',
|
||||
},
|
||||
'type': 'bs_target_practice.TargetPracticeGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Hold Time': 30,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 300,
|
||||
'map': 'Tip Top'
|
||||
{
|
||||
'settings': {
|
||||
'Hold Time': 30,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 300,
|
||||
'map': 'Tip Top',
|
||||
},
|
||||
'type': 'bs_king_of_the_hill.KingOfTheHillGame',
|
||||
},
|
||||
'type': 'bs_king_of_the_hill.KingOfTheHillGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'Respawn Times': 1.0,
|
||||
'Score to Win': 2,
|
||||
'Time Limit': 300,
|
||||
'map': 'Zigzag'
|
||||
{
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'Respawn Times': 1.0,
|
||||
'Score to Win': 2,
|
||||
'Time Limit': 300,
|
||||
'map': 'Zigzag',
|
||||
},
|
||||
'type': 'bs_assault.AssaultGame',
|
||||
},
|
||||
'type': 'bs_assault.AssaultGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'Flag Idle Return Time': 30,
|
||||
'Flag Touch Return Time': 0,
|
||||
'Respawn Times': 1.0,
|
||||
'Score to Win': 3,
|
||||
'Time Limit': 300,
|
||||
'map': 'Happy Thoughts'
|
||||
{
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'Flag Idle Return Time': 30,
|
||||
'Flag Touch Return Time': 0,
|
||||
'Respawn Times': 1.0,
|
||||
'Score to Win': 3,
|
||||
'Time Limit': 300,
|
||||
'map': 'Happy Thoughts',
|
||||
},
|
||||
'type': 'bs_capture_the_flag.CTFGame',
|
||||
},
|
||||
'type': 'bs_capture_the_flag.CTFGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Bomb Spawning': 1000,
|
||||
'Epic Mode': True,
|
||||
'Laps': 1,
|
||||
'Mine Spawning': 2000,
|
||||
'Time Limit': 300,
|
||||
'map': 'Big G'
|
||||
{
|
||||
'settings': {
|
||||
'Bomb Spawning': 1000,
|
||||
'Epic Mode': True,
|
||||
'Laps': 1,
|
||||
'Mine Spawning': 2000,
|
||||
'Time Limit': 300,
|
||||
'map': 'Big G',
|
||||
},
|
||||
'type': 'bs_race.RaceGame',
|
||||
},
|
||||
'type': 'bs_race.RaceGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'Kills to Win Per Player': 5,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 300,
|
||||
'map': 'Monkey Face'
|
||||
{
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'Kills to Win Per Player': 5,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 300,
|
||||
'map': 'Monkey Face',
|
||||
},
|
||||
'type': 'bs_death_match.DeathMatchGame',
|
||||
},
|
||||
'type': 'bs_death_match.DeathMatchGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Hold Time': 30,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 300,
|
||||
'map': 'Lake Frigid'
|
||||
{
|
||||
'settings': {
|
||||
'Hold Time': 30,
|
||||
'Respawn Times': 1.0,
|
||||
'Time Limit': 300,
|
||||
'map': 'Lake Frigid',
|
||||
},
|
||||
'type': 'bs_keep_away.KeepAwayGame',
|
||||
},
|
||||
'type': 'bs_keep_away.KeepAwayGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'Flag Idle Return Time': 30,
|
||||
'Flag Touch Return Time': 3,
|
||||
'Respawn Times': 1.0,
|
||||
'Score to Win': 2,
|
||||
'Time Limit': 300,
|
||||
'map': 'Tip Top'
|
||||
{
|
||||
'settings': {
|
||||
'Epic Mode': False,
|
||||
'Flag Idle Return Time': 30,
|
||||
'Flag Touch Return Time': 3,
|
||||
'Respawn Times': 1.0,
|
||||
'Score to Win': 2,
|
||||
'Time Limit': 300,
|
||||
'map': 'Tip Top',
|
||||
},
|
||||
'type': 'bs_capture_the_flag.CTFGame',
|
||||
},
|
||||
'type': 'bs_capture_the_flag.CTFGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Balance Total Lives': False,
|
||||
'Epic Mode': False,
|
||||
'Lives Per Player': 3,
|
||||
'Respawn Times': 1.0,
|
||||
'Solo Mode': False,
|
||||
'Time Limit': 300,
|
||||
'map': 'Crag Castle'
|
||||
{
|
||||
'settings': {
|
||||
'Balance Total Lives': False,
|
||||
'Epic Mode': False,
|
||||
'Lives Per Player': 3,
|
||||
'Respawn Times': 1.0,
|
||||
'Solo Mode': False,
|
||||
'Time Limit': 300,
|
||||
'map': 'Crag Castle',
|
||||
},
|
||||
'type': 'bs_elimination.EliminationGame',
|
||||
},
|
||||
'type': 'bs_elimination.EliminationGame'
|
||||
}, {
|
||||
'settings': {
|
||||
'Epic Mode': True,
|
||||
'Respawn Times': 0.25,
|
||||
'Time Limit': 120,
|
||||
'map': 'Zigzag'
|
||||
{
|
||||
'settings': {
|
||||
'Epic Mode': True,
|
||||
'Respawn Times': 0.25,
|
||||
'Time Limit': 120,
|
||||
'map': 'Zigzag',
|
||||
},
|
||||
'type': 'bs_conquest.ConquestGame',
|
||||
},
|
||||
'type': 'bs_conquest.ConquestGame'
|
||||
}]
|
||||
]
|
||||
|
|
|
|||
67
dist/ba_data/python/ba/_plugin.py
vendored
67
dist/ba_data/python/ba/_plugin.py
vendored
|
|
@ -42,9 +42,12 @@ class PluginSubsystem:
|
|||
# Create a potential-plugin for each class we found in the scan.
|
||||
for class_path in results.exports_of_class(Plugin):
|
||||
plugs.potential_plugins.append(
|
||||
PotentialPlugin(display_name=Lstr(value=class_path),
|
||||
class_path=class_path,
|
||||
available=True))
|
||||
PotentialPlugin(
|
||||
display_name=Lstr(value=class_path),
|
||||
class_path=class_path,
|
||||
available=True,
|
||||
)
|
||||
)
|
||||
if class_path not in plugstates:
|
||||
# Go ahead and enable new plugins by default, but we'll
|
||||
# inform the user that they need to restart to pick them up.
|
||||
|
|
@ -59,8 +62,9 @@ class PluginSubsystem:
|
|||
# plugins, so we don't need the message about 'restart to activate'
|
||||
# anymore.
|
||||
if found_new and bool(False):
|
||||
_ba.screenmessage(Lstr(resource='pluginsDetectedText'),
|
||||
color=(0, 1, 0))
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='pluginsDetectedText'), color=(0, 1, 0)
|
||||
)
|
||||
_ba.playsound(_ba.getsound('ding'))
|
||||
|
||||
if config_changed:
|
||||
|
|
@ -75,6 +79,7 @@ class PluginSubsystem:
|
|||
plugin.on_app_running()
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception('Error in plugin on_app_running()')
|
||||
|
||||
def on_app_pause(self) -> None:
|
||||
|
|
@ -84,6 +89,7 @@ class PluginSubsystem:
|
|||
plugin.on_app_pause()
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception('Error in plugin on_app_pause()')
|
||||
|
||||
def on_app_resume(self) -> None:
|
||||
|
|
@ -93,6 +99,7 @@ class PluginSubsystem:
|
|||
plugin.on_app_resume()
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception('Error in plugin on_app_resume()')
|
||||
|
||||
def on_app_shutdown(self) -> None:
|
||||
|
|
@ -102,6 +109,7 @@ class PluginSubsystem:
|
|||
plugin.on_app_shutdown()
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception('Error in plugin on_app_shutdown()')
|
||||
|
||||
def load_plugins(self) -> None:
|
||||
|
|
@ -113,8 +121,9 @@ class PluginSubsystem:
|
|||
# in the app config. Its not our job to look at meta stuff here.
|
||||
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))
|
||||
plugkeys: list[str] = sorted(
|
||||
key for key, val in plugstates.items() if val.get('enabled', False)
|
||||
)
|
||||
disappeared_plugs: set[str] = set()
|
||||
for plugkey in plugkeys:
|
||||
try:
|
||||
|
|
@ -124,10 +133,13 @@ class PluginSubsystem:
|
|||
continue
|
||||
except Exception as exc:
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
_ba.screenmessage(Lstr(resource='pluginClassLoadErrorText',
|
||||
subs=[('${PLUGIN}', plugkey),
|
||||
('${ERROR}', str(exc))]),
|
||||
color=(1, 0, 0))
|
||||
_ba.screenmessage(
|
||||
Lstr(
|
||||
resource='pluginClassLoadErrorText',
|
||||
subs=[('${PLUGIN}', plugkey), ('${ERROR}', str(exc))],
|
||||
),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
logging.exception("Error loading plugin class '%s'", plugkey)
|
||||
continue
|
||||
try:
|
||||
|
|
@ -136,11 +148,15 @@ class PluginSubsystem:
|
|||
self.active_plugins[plugkey] = plugin
|
||||
except Exception as exc:
|
||||
from ba import _error
|
||||
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
_ba.screenmessage(Lstr(resource='pluginInitErrorText',
|
||||
subs=[('${PLUGIN}', plugkey),
|
||||
('${ERROR}', str(exc))]),
|
||||
color=(1, 0, 0))
|
||||
_ba.screenmessage(
|
||||
Lstr(
|
||||
resource='pluginInitErrorText',
|
||||
subs=[('${PLUGIN}', plugkey), ('${ERROR}', str(exc))],
|
||||
),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
_error.print_exception(f"Error initing plugin: '{plugkey}'.")
|
||||
|
||||
# If plugins disappeared, let the user know gently and remove them
|
||||
|
|
@ -150,13 +166,18 @@ class PluginSubsystem:
|
|||
if disappeared_plugs:
|
||||
_ba.playsound(_ba.getsound('shieldDown'))
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='pluginsRemovedText',
|
||||
subs=[('${NUM}', str(len(disappeared_plugs)))]),
|
||||
Lstr(
|
||||
resource='pluginsRemovedText',
|
||||
subs=[('${NUM}', str(len(disappeared_plugs)))],
|
||||
),
|
||||
color=(1, 1, 0),
|
||||
)
|
||||
plugnames = ', '.join(disappeared_plugs)
|
||||
logging.warning('%d plugin(s) no longer found: %s.',
|
||||
len(disappeared_plugs), plugnames)
|
||||
logging.warning(
|
||||
'%d plugin(s) no longer found: %s.',
|
||||
len(disappeared_plugs),
|
||||
plugnames,
|
||||
)
|
||||
for goneplug in disappeared_plugs:
|
||||
del _ba.app.config['Plugins'][goneplug]
|
||||
_ba.app.config.commit()
|
||||
|
|
@ -173,6 +194,7 @@ class PotentialPlugin:
|
|||
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
|
||||
|
|
@ -200,3 +222,10 @@ class Plugin:
|
|||
|
||||
def on_app_shutdown(self) -> None:
|
||||
"""Called before closing the application."""
|
||||
|
||||
def has_settings_ui(self) -> bool:
|
||||
"""Called to ask if we have settings UI we can show."""
|
||||
return False
|
||||
|
||||
def show_settings_ui(self, source_widget: ba.Widget | None) -> None:
|
||||
"""Called to show our settings UI."""
|
||||
|
|
|
|||
14
dist/ba_data/python/ba/_powerup.py
vendored
14
dist/ba_data/python/ba/_powerup.py
vendored
|
|
@ -45,6 +45,14 @@ class PowerupAcceptMessage:
|
|||
|
||||
def get_default_powerup_distribution() -> Sequence[tuple[str, int]]:
|
||||
"""Standard set of powerups."""
|
||||
return (('triple_bombs', 3), ('ice_bombs', 3), ('punch', 3),
|
||||
('impact_bombs', 3), ('land_mines', 2), ('sticky_bombs', 3),
|
||||
('shield', 2), ('health', 1), ('curse', 1))
|
||||
return (
|
||||
('triple_bombs', 3),
|
||||
('ice_bombs', 3),
|
||||
('punch', 3),
|
||||
('impact_bombs', 3),
|
||||
('land_mines', 2),
|
||||
('sticky_bombs', 3),
|
||||
('shield', 2),
|
||||
('health', 1),
|
||||
('curse', 1),
|
||||
)
|
||||
|
|
|
|||
38
dist/ba_data/python/ba/_profile.py
vendored
38
dist/ba_data/python/ba/_profile.py
vendored
|
|
@ -13,11 +13,24 @@ if TYPE_CHECKING:
|
|||
|
||||
# NOTE: player color options are enforced server-side for non-pro accounts
|
||||
# so don't change these or they won't stick...
|
||||
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)]
|
||||
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),
|
||||
]
|
||||
|
||||
|
||||
def get_player_colors() -> list[tuple[float, float, float]]:
|
||||
|
|
@ -49,8 +62,7 @@ def get_player_profile_icon(profilename: str) -> str:
|
|||
|
||||
|
||||
def get_player_profile_colors(
|
||||
profilename: str | None,
|
||||
profiles: dict[str, dict[str, Any]] | None = None
|
||||
profilename: str | None, profiles: dict[str, dict[str, Any]] | None = None
|
||||
) -> tuple[tuple[float, float, float], tuple[float, float, float]]:
|
||||
"""Given a profile, return colors for them."""
|
||||
appconfig = _ba.app.config
|
||||
|
|
@ -83,11 +95,13 @@ def get_player_profile_colors(
|
|||
if profilename is None:
|
||||
# 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)]
|
||||
highlight = PLAYER_COLORS[
|
||||
random.randrange(len(PLAYER_COLORS) - 2)
|
||||
]
|
||||
else:
|
||||
highlight = PLAYER_COLORS[sum(ord(c) + 1
|
||||
for c in profilename) %
|
||||
(len(PLAYER_COLORS) - 2)]
|
||||
highlight = PLAYER_COLORS[
|
||||
sum(ord(c) + 1 for c in profilename)
|
||||
% (len(PLAYER_COLORS) - 2)
|
||||
]
|
||||
|
||||
return color, highlight
|
||||
|
|
|
|||
1
dist/ba_data/python/ba/_score.py
vendored
1
dist/ba_data/python/ba/_score.py
vendored
|
|
@ -18,6 +18,7 @@ class ScoreType(Enum):
|
|||
|
||||
Category: **Enums**
|
||||
"""
|
||||
|
||||
SECONDS = 's'
|
||||
MILLISECONDS = 'ms'
|
||||
POINTS = 'p'
|
||||
|
|
|
|||
203
dist/ba_data/python/ba/_servermode.py
vendored
203
dist/ba_data/python/ba/_servermode.py
vendored
|
|
@ -9,13 +9,18 @@ import logging
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
from efro.terminal import Clr
|
||||
from bacommon.servermanager import (ServerCommand, StartServerModeCommand,
|
||||
ShutdownCommand, ShutdownReason,
|
||||
ChatMessageCommand, ScreenMessageCommand,
|
||||
ClientListCommand, KickCommand)
|
||||
from bacommon.servermanager import (
|
||||
ServerCommand,
|
||||
StartServerModeCommand,
|
||||
ShutdownCommand,
|
||||
ShutdownReason,
|
||||
ChatMessageCommand,
|
||||
ScreenMessageCommand,
|
||||
ClientListCommand,
|
||||
KickCommand,
|
||||
)
|
||||
import _ba
|
||||
from ba._internal import (add_transaction, run_transactions,
|
||||
get_v1_account_state)
|
||||
from ba._internal import add_transaction, run_transactions, get_v1_account_state
|
||||
from ba._generated.enums import TimeType
|
||||
from ba._freeforallsession import FreeForAllSession
|
||||
from ba._dualteamsession import DualTeamSession
|
||||
|
|
@ -31,6 +36,7 @@ if TYPE_CHECKING:
|
|||
def _cmd(command_data: bytes) -> None:
|
||||
"""Handle commands coming in from our server manager parent process."""
|
||||
import pickle
|
||||
|
||||
command = pickle.loads(command_data)
|
||||
assert isinstance(command, ServerCommand)
|
||||
|
||||
|
|
@ -41,8 +47,9 @@ def _cmd(command_data: bytes) -> None:
|
|||
|
||||
if isinstance(command, ShutdownCommand):
|
||||
assert _ba.app.server is not None
|
||||
_ba.app.server.shutdown(reason=command.reason,
|
||||
immediate=command.immediate)
|
||||
_ba.app.server.shutdown(
|
||||
reason=command.reason, immediate=command.immediate
|
||||
)
|
||||
return
|
||||
|
||||
if isinstance(command, ChatMessageCommand):
|
||||
|
|
@ -56,10 +63,12 @@ def _cmd(command_data: bytes) -> None:
|
|||
# Note: we have to do transient messages if
|
||||
# clients is specified, so they won't show up
|
||||
# in replays.
|
||||
_ba.screenmessage(command.message,
|
||||
color=command.color,
|
||||
clients=command.clients,
|
||||
transient=command.clients is not None)
|
||||
_ba.screenmessage(
|
||||
command.message,
|
||||
color=command.color,
|
||||
clients=command.clients,
|
||||
transient=command.clients is not None,
|
||||
)
|
||||
return
|
||||
|
||||
if isinstance(command, ClientListCommand):
|
||||
|
|
@ -69,12 +78,15 @@ def _cmd(command_data: bytes) -> None:
|
|||
|
||||
if isinstance(command, KickCommand):
|
||||
assert _ba.app.server is not None
|
||||
_ba.app.server.kick(client_id=command.client_id,
|
||||
ban_time=command.ban_time)
|
||||
_ba.app.server.kick(
|
||||
client_id=command.client_id, ban_time=command.ban_time
|
||||
)
|
||||
return
|
||||
|
||||
print(f'{Clr.SRED}ERROR: server process'
|
||||
f' got unknown command: {type(command)}{Clr.RST}')
|
||||
print(
|
||||
f'{Clr.SRED}ERROR: server process'
|
||||
f' got unknown command: {type(command)}{Clr.RST}'
|
||||
)
|
||||
|
||||
|
||||
class ServerController:
|
||||
|
|
@ -105,23 +117,28 @@ class ServerController:
|
|||
# account sign-in or fetching playlists; this will kick off the
|
||||
# session once done.
|
||||
with _ba.Context('ui'):
|
||||
self._prep_timer = _ba.Timer(0.25,
|
||||
self._prepare_to_serve,
|
||||
timetype=TimeType.REAL,
|
||||
repeat=True)
|
||||
self._prep_timer = _ba.Timer(
|
||||
0.25,
|
||||
self._prepare_to_serve,
|
||||
timetype=TimeType.REAL,
|
||||
repeat=True,
|
||||
)
|
||||
|
||||
def print_client_list(self) -> None:
|
||||
"""Print info about all connected clients."""
|
||||
import json
|
||||
|
||||
roster = _ba.get_game_roster()
|
||||
title1 = 'Client ID'
|
||||
title2 = 'Account Name'
|
||||
title3 = 'Players'
|
||||
col1 = 10
|
||||
col2 = 16
|
||||
out = (f'{Clr.BLD}'
|
||||
f'{title1:<{col1}} {title2:<{col2}} {title3}'
|
||||
f'{Clr.RST}')
|
||||
out = (
|
||||
f'{Clr.BLD}'
|
||||
f'{title1:<{col1}} {title2:<{col2}} {title3}'
|
||||
f'{Clr.RST}'
|
||||
)
|
||||
for client in roster:
|
||||
if client['client_id'] == -1:
|
||||
continue
|
||||
|
|
@ -153,9 +170,11 @@ class ServerController:
|
|||
print(f'{Clr.SBLU}Immediate shutdown initiated.{Clr.RST}')
|
||||
self._execute_shutdown()
|
||||
else:
|
||||
print(f'{Clr.SBLU}Shutdown initiated;'
|
||||
f' server process will exit at the next clean opportunity.'
|
||||
f'{Clr.RST}')
|
||||
print(
|
||||
f'{Clr.SBLU}Shutdown initiated;'
|
||||
f' server process will exit at the next clean opportunity.'
|
||||
f'{Clr.RST}'
|
||||
)
|
||||
|
||||
def handle_transition(self) -> bool:
|
||||
"""Handle transitioning to a new ba.Session or quitting the app.
|
||||
|
|
@ -172,37 +191,45 @@ class ServerController:
|
|||
|
||||
def _execute_shutdown(self) -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
if self._executing_shutdown:
|
||||
return
|
||||
self._executing_shutdown = True
|
||||
timestrval = time.strftime('%c')
|
||||
if self._shutdown_reason is ShutdownReason.RESTARTING:
|
||||
_ba.screenmessage(Lstr(resource='internal.serverRestartingText'),
|
||||
color=(1, 0.5, 0.0))
|
||||
print(f'{Clr.SBLU}Exiting for server-restart'
|
||||
f' at {timestrval}.{Clr.RST}')
|
||||
_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}'
|
||||
)
|
||||
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}')
|
||||
_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}'
|
||||
)
|
||||
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._net import master_server_get
|
||||
|
||||
master_server_get(
|
||||
'bsAccessCheck',
|
||||
{
|
||||
'port': _ba.get_game_port(),
|
||||
'b': _ba.app.build_number
|
||||
},
|
||||
{'port': _ba.get_game_port(), 'b': _ba.app.build_number},
|
||||
callback=self._access_check_response,
|
||||
)
|
||||
|
||||
def _access_check_response(self, data: dict[str, Any] | None) -> None:
|
||||
import os
|
||||
|
||||
if data is None:
|
||||
print('error on UDP port access check (internet down?)')
|
||||
else:
|
||||
|
|
@ -216,17 +243,22 @@ class ServerController:
|
|||
addrstr = ''
|
||||
poststr = (
|
||||
'\nSet environment variable BA_ACCESS_CHECK_VERBOSE=1'
|
||||
' for more info.')
|
||||
' for more info.'
|
||||
)
|
||||
if data['accessible']:
|
||||
print(f'{Clr.SBLU}Master server access check of{addrstr}'
|
||||
f' udp port {port} succeeded.\n'
|
||||
f'Your server appears to be'
|
||||
f' joinable from the internet.{poststr}{Clr.RST}')
|
||||
print(
|
||||
f'{Clr.SBLU}Master server access check of{addrstr}'
|
||||
f' udp port {port} succeeded.\n'
|
||||
f'Your server appears to be'
|
||||
f' joinable from the internet.{poststr}{Clr.RST}'
|
||||
)
|
||||
else:
|
||||
print(f'{Clr.SRED}Master server access check of{addrstr}'
|
||||
f' udp port {port} failed.\n'
|
||||
f'Your server does not appear to be'
|
||||
f' joinable from the internet.{poststr}{Clr.RST}')
|
||||
print(
|
||||
f'{Clr.SRED}Master server access check of{addrstr}'
|
||||
f' udp port {port} failed.\n'
|
||||
f'Your server does not appear to be'
|
||||
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."""
|
||||
|
|
@ -248,15 +280,18 @@ class ServerController:
|
|||
can_launch = True
|
||||
else:
|
||||
if not self._playlist_fetch_sent_request:
|
||||
print(f'{Clr.SBLU}Requesting shared-playlist'
|
||||
f' {self._config.playlist_code}...{Clr.RST}')
|
||||
print(
|
||||
f'{Clr.SBLU}Requesting shared-playlist'
|
||||
f' {self._config.playlist_code}...{Clr.RST}'
|
||||
)
|
||||
add_transaction(
|
||||
{
|
||||
'type': 'IMPORT_PLAYLIST',
|
||||
'code': str(self._config.playlist_code),
|
||||
'overwrite': True
|
||||
'overwrite': True,
|
||||
},
|
||||
callback=self._on_playlist_fetch_response)
|
||||
callback=self._on_playlist_fetch_response,
|
||||
)
|
||||
run_transactions()
|
||||
self._playlist_fetch_sent_request = True
|
||||
|
||||
|
|
@ -278,13 +313,17 @@ class ServerController:
|
|||
|
||||
# Once we get here, simply modify our config to use this playlist.
|
||||
typename = (
|
||||
'teams' if result['playlistType'] == 'Team Tournament' else
|
||||
'ffa' if result['playlistType'] == 'Free-for-All' else '??')
|
||||
'teams'
|
||||
if result['playlistType'] == 'Team Tournament'
|
||||
else 'ffa'
|
||||
if result['playlistType'] == 'Free-for-All'
|
||||
else '??'
|
||||
)
|
||||
plistname = result['playlistName']
|
||||
print(f'{Clr.SBLU}Got playlist: "{plistname}" ({typename}).{Clr.RST}')
|
||||
self._playlist_fetch_got_response = True
|
||||
self._config.session_type = typename
|
||||
self._playlist_name = (result['playlistName'])
|
||||
self._playlist_name = result['playlistName']
|
||||
|
||||
def _get_session_type(self) -> type[ba.Session]:
|
||||
# Convert string session type to the class.
|
||||
|
|
@ -296,7 +335,8 @@ class ServerController:
|
|||
if self._config.session_type == 'coop':
|
||||
return CoopSession
|
||||
raise RuntimeError(
|
||||
f'Invalid session_type: "{self._config.session_type}"')
|
||||
f'Invalid session_type: "{self._config.session_type}"'
|
||||
)
|
||||
|
||||
def _launch_server_session(self) -> None:
|
||||
"""Kick off a host-session based on the current server config."""
|
||||
|
|
@ -306,13 +346,17 @@ class ServerController:
|
|||
sessiontype = self._get_session_type()
|
||||
|
||||
if get_v1_account_state() != 'signed_in':
|
||||
print('WARNING: launch_server_session() expects to run '
|
||||
'with a signed in server account')
|
||||
print(
|
||||
'WARNING: launch_server_session() expects to run '
|
||||
'with a signed in server account'
|
||||
)
|
||||
|
||||
# If we didn't fetch a playlist but there's an inline one in the
|
||||
# server-config, pull it in to the game config and use it.
|
||||
if (self._config.playlist_code is None
|
||||
and self._config.playlist_inline is not None):
|
||||
if (
|
||||
self._config.playlist_code is None
|
||||
and self._config.playlist_inline is not None
|
||||
):
|
||||
self._playlist_name = 'ServerModePlaylist'
|
||||
if sessiontype is FreeForAllSession:
|
||||
ptypename = 'Free-for-All'
|
||||
|
|
@ -325,12 +369,14 @@ class ServerController:
|
|||
|
||||
# Need to add this in a transaction instead of just setting
|
||||
# it directly or it will get overwritten by the master-server.
|
||||
add_transaction({
|
||||
'type': 'ADD_PLAYLIST',
|
||||
'playlistType': ptypename,
|
||||
'playlistName': self._playlist_name,
|
||||
'playlist': self._config.playlist_inline
|
||||
})
|
||||
add_transaction(
|
||||
{
|
||||
'type': 'ADD_PLAYLIST',
|
||||
'playlistType': ptypename,
|
||||
'playlistName': self._playlist_name,
|
||||
'playlist': self._config.playlist_inline,
|
||||
}
|
||||
)
|
||||
run_transactions()
|
||||
|
||||
if self._first_run:
|
||||
|
|
@ -338,17 +384,20 @@ class ServerController:
|
|||
startupmsg = (
|
||||
f'{Clr.BLD}{Clr.BLU}{_ba.appnameupper()} {app.version}'
|
||||
f' ({app.build_number})'
|
||||
f' entering server-mode {curtimestr}{Clr.RST}')
|
||||
f' entering server-mode {curtimestr}{Clr.RST}'
|
||||
)
|
||||
logging.info(startupmsg)
|
||||
|
||||
if sessiontype is FreeForAllSession:
|
||||
appcfg['Free-for-All Playlist Selection'] = self._playlist_name
|
||||
appcfg['Free-for-All Playlist Randomize'] = (
|
||||
self._config.playlist_shuffle)
|
||||
appcfg[
|
||||
'Free-for-All Playlist Randomize'
|
||||
] = self._config.playlist_shuffle
|
||||
elif sessiontype is DualTeamSession:
|
||||
appcfg['Team Tournament Playlist Selection'] = self._playlist_name
|
||||
appcfg['Team Tournament Playlist Randomize'] = (
|
||||
self._config.playlist_shuffle)
|
||||
appcfg[
|
||||
'Team Tournament Playlist Randomize'
|
||||
] = self._config.playlist_shuffle
|
||||
elif sessiontype is CoopSession:
|
||||
app.coop_session_args = {
|
||||
'campaign': self._config.coop_campaign,
|
||||
|
|
@ -363,7 +412,8 @@ class ServerController:
|
|||
_ba.set_authenticate_clients(self._config.authenticate_clients)
|
||||
|
||||
_ba.set_enable_default_kick_voting(
|
||||
self._config.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).
|
||||
|
|
@ -376,10 +426,13 @@ class ServerController:
|
|||
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)
|
||||
|
||||
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)
|
||||
|
||||
|
|
|
|||
195
dist/ba_data/python/ba/_session.py
vendored
195
dist/ba_data/python/ba/_session.py
vendored
|
|
@ -70,12 +70,14 @@ class Session:
|
|||
"""All the ba.SessionTeams in the Session. Most things should use the
|
||||
list of ba.Team-s in ba.Activity; not this."""
|
||||
|
||||
def __init__(self,
|
||||
depsets: Sequence[ba.DependencySet],
|
||||
team_names: Sequence[str] | None = None,
|
||||
team_colors: Sequence[Sequence[float]] | None = None,
|
||||
min_players: int = 1,
|
||||
max_players: int = 8):
|
||||
def __init__(
|
||||
self,
|
||||
depsets: Sequence[ba.DependencySet],
|
||||
team_names: Sequence[str] | None = None,
|
||||
team_colors: Sequence[Sequence[float]] | None = None,
|
||||
min_players: int = 1,
|
||||
max_players: int = 8,
|
||||
):
|
||||
"""Instantiate a session.
|
||||
|
||||
depsets should be a sequence of successfully resolved ba.DependencySet
|
||||
|
|
@ -116,10 +118,12 @@ class Session:
|
|||
|
||||
# Throw a combined exception if we found anything missing.
|
||||
if missing_asset_packages:
|
||||
raise DependencyError([
|
||||
Dependency(AssetPackage, set_id)
|
||||
for set_id in missing_asset_packages
|
||||
])
|
||||
raise DependencyError(
|
||||
[
|
||||
Dependency(AssetPackage, set_id)
|
||||
for set_id in missing_asset_packages
|
||||
]
|
||||
)
|
||||
|
||||
# Ok; looks like our dependencies check out.
|
||||
# Now give the engine a list of asset-set-ids to pass along to clients.
|
||||
|
|
@ -152,27 +156,33 @@ class Session:
|
|||
self._wants_to_end = False
|
||||
self._ending = False
|
||||
self._activity_should_end_immediately = False
|
||||
self._activity_should_end_immediately_results: (ba.GameResults
|
||||
| None) = None
|
||||
self._activity_should_end_immediately_results: (
|
||||
ba.GameResults | None
|
||||
) = None
|
||||
self._activity_should_end_immediately_delay = 0.0
|
||||
|
||||
# Create static teams if we're using them.
|
||||
if self.use_teams:
|
||||
if team_names is None:
|
||||
raise RuntimeError(
|
||||
'use_teams is True but team_names not provided.')
|
||||
'use_teams is True but team_names not provided.'
|
||||
)
|
||||
if team_colors is None:
|
||||
raise RuntimeError(
|
||||
'use_teams is True but team_colors not provided.')
|
||||
'use_teams is True but team_colors not provided.'
|
||||
)
|
||||
if len(team_colors) != len(team_names):
|
||||
raise RuntimeError(f'Got {len(team_names)} team_names'
|
||||
f' and {len(team_colors)} team_colors;'
|
||||
f' these numbers must match.')
|
||||
raise RuntimeError(
|
||||
f'Got {len(team_names)} team_names'
|
||||
f' and {len(team_colors)} team_colors;'
|
||||
f' these numbers must match.'
|
||||
)
|
||||
for i, color in enumerate(team_colors):
|
||||
team = SessionTeam(team_id=self._next_team_id,
|
||||
name=GameActivity.get_team_display_string(
|
||||
team_names[i]),
|
||||
color=color)
|
||||
team = SessionTeam(
|
||||
team_id=self._next_team_id,
|
||||
name=GameActivity.get_team_display_string(team_names[i]),
|
||||
color=color,
|
||||
)
|
||||
self.sessionteams.append(team)
|
||||
self._next_team_id += 1
|
||||
try:
|
||||
|
|
@ -218,12 +228,15 @@ class Session:
|
|||
# Print a rejection message *only* to the client trying to
|
||||
# join (prevents spamming everyone else in the game).
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
_ba.screenmessage(Lstr(resource='playerLimitReachedText',
|
||||
subs=[('${COUNT}',
|
||||
str(self.max_players))]),
|
||||
color=(0.8, 0.0, 0.0),
|
||||
clients=[player.inputdevice.client_id],
|
||||
transient=True)
|
||||
_ba.screenmessage(
|
||||
Lstr(
|
||||
resource='playerLimitReachedText',
|
||||
subs=[('${COUNT}', str(self.max_players))],
|
||||
),
|
||||
color=(0.8, 0.0, 0.0),
|
||||
clients=[player.inputdevice.client_id],
|
||||
transient=True,
|
||||
)
|
||||
return False
|
||||
|
||||
_ba.playsound(_ba.getsound('dripity'))
|
||||
|
|
@ -233,8 +246,10 @@ class Session:
|
|||
"""Called when a previously-accepted ba.SessionPlayer leaves."""
|
||||
|
||||
if sessionplayer not in self.sessionplayers:
|
||||
print('ERROR: Session.on_player_leave called'
|
||||
' for player not in our list.')
|
||||
print(
|
||||
'ERROR: Session.on_player_leave called'
|
||||
' for player not in our list.'
|
||||
)
|
||||
return
|
||||
|
||||
_ba.playsound(_ba.getsound('playerLeft'))
|
||||
|
|
@ -256,15 +271,20 @@ class Session:
|
|||
assert sessionteam is not None
|
||||
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='playerLeftText',
|
||||
subs=[('${PLAYER}', sessionplayer.getname(full=True))]))
|
||||
Lstr(
|
||||
resource='playerLeftText',
|
||||
subs=[('${PLAYER}', sessionplayer.getname(full=True))],
|
||||
)
|
||||
)
|
||||
|
||||
# Remove them from their SessionTeam.
|
||||
if sessionplayer in sessionteam.players:
|
||||
sessionteam.players.remove(sessionplayer)
|
||||
else:
|
||||
print('SessionPlayer not found in SessionTeam'
|
||||
' in on_player_leave.')
|
||||
print(
|
||||
'SessionPlayer not found in SessionTeam'
|
||||
' in on_player_leave.'
|
||||
)
|
||||
|
||||
# Grab their activity-specific player instance.
|
||||
player = sessionplayer.activityplayer
|
||||
|
|
@ -284,8 +304,9 @@ class Session:
|
|||
# Now remove them from the session list.
|
||||
self.sessionplayers.remove(sessionplayer)
|
||||
|
||||
def _remove_player_team(self, sessionteam: ba.SessionTeam,
|
||||
activity: ba.Activity | None) -> None:
|
||||
def _remove_player_team(
|
||||
self, sessionteam: ba.SessionTeam, activity: ba.Activity | None
|
||||
) -> None:
|
||||
"""Remove the player-specific team in non-teams mode."""
|
||||
|
||||
# They should have been the only one on their team.
|
||||
|
|
@ -306,14 +327,17 @@ class Session:
|
|||
self.on_team_leave(sessionteam)
|
||||
except Exception:
|
||||
print_exception(
|
||||
f'Error in on_team_leave for Session {self}.')
|
||||
f'Error in on_team_leave for Session {self}.'
|
||||
)
|
||||
else:
|
||||
print('Team no in Session teams in on_player_leave.')
|
||||
try:
|
||||
sessionteam.leave()
|
||||
except Exception:
|
||||
print_exception(f'Error clearing sessiondata'
|
||||
f' for team {sessionteam} in session {self}.')
|
||||
print_exception(
|
||||
f'Error clearing sessiondata'
|
||||
f' for team {sessionteam} in session {self}.'
|
||||
)
|
||||
|
||||
def end(self) -> None:
|
||||
"""Initiates an end to the session and a return to the main menu.
|
||||
|
|
@ -329,17 +353,20 @@ class Session:
|
|||
"""(internal)"""
|
||||
from ba._activitytypes import EndSessionActivity
|
||||
from ba._generated.enums import TimeType
|
||||
|
||||
with _ba.Context(self):
|
||||
curtime = _ba.time(TimeType.REAL)
|
||||
if self._ending:
|
||||
# Ignore repeats unless its been a while.
|
||||
assert self._launch_end_session_activity_time is not None
|
||||
since_last = (curtime - self._launch_end_session_activity_time)
|
||||
since_last = curtime - self._launch_end_session_activity_time
|
||||
if since_last < 30.0:
|
||||
return
|
||||
print_error(
|
||||
'_launch_end_session_activity called twice (since_last=' +
|
||||
str(since_last) + ')')
|
||||
'_launch_end_session_activity called twice (since_last='
|
||||
+ str(since_last)
|
||||
+ ')'
|
||||
)
|
||||
self._launch_end_session_activity_time = curtime
|
||||
self.setactivity(_ba.newactivity(EndSessionActivity))
|
||||
self._wants_to_end = False
|
||||
|
|
@ -351,8 +378,9 @@ class Session:
|
|||
def on_team_leave(self, team: ba.SessionTeam) -> None:
|
||||
"""Called when a ba.Team is leaving the session."""
|
||||
|
||||
def end_activity(self, activity: ba.Activity, results: Any, delay: float,
|
||||
force: bool) -> None:
|
||||
def end_activity(
|
||||
self, activity: ba.Activity, results: Any, delay: float, force: bool
|
||||
) -> None:
|
||||
"""Commence shutdown of a ba.Activity (if not already occurring).
|
||||
|
||||
'delay' is the time delay before the Activity actually ends
|
||||
|
|
@ -385,7 +413,8 @@ class Session:
|
|||
self._activity_end_timer = _ba.Timer(
|
||||
delay,
|
||||
Call(self._complete_end_activity, activity, results),
|
||||
timetype=TimeType.BASE)
|
||||
timetype=TimeType.BASE,
|
||||
)
|
||||
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
"""General message handling; can be passed any message object."""
|
||||
|
|
@ -407,7 +436,6 @@ class Session:
|
|||
return None
|
||||
|
||||
class _SetActivityScopedLock:
|
||||
|
||||
def __init__(self, session: ba.Session) -> None:
|
||||
self._session = session
|
||||
if session._in_set_activity:
|
||||
|
|
@ -442,12 +470,16 @@ class Session:
|
|||
return
|
||||
|
||||
if self._next_activity is not None:
|
||||
raise RuntimeError('Activity switch already in progress (to ' +
|
||||
str(self._next_activity) + ')')
|
||||
raise RuntimeError(
|
||||
'Activity switch already in progress (to '
|
||||
+ str(self._next_activity)
|
||||
+ ')'
|
||||
)
|
||||
|
||||
prev_activity = self._activity_retained
|
||||
prev_globals = (prev_activity.globalsnode
|
||||
if prev_activity is not None else None)
|
||||
prev_globals = (
|
||||
prev_activity.globalsnode if prev_activity is not None else None
|
||||
)
|
||||
|
||||
# Let the activity do its thing.
|
||||
activity.transition_in(prev_globals)
|
||||
|
|
@ -476,9 +508,11 @@ class Session:
|
|||
# will trigger the next activity to run).
|
||||
if prev_activity is not None:
|
||||
with _ba.Context('ui'):
|
||||
_ba.timer(max(0.0, activity.transition_time),
|
||||
prev_activity.expire,
|
||||
timetype=TimeType.REAL)
|
||||
_ba.timer(
|
||||
max(0.0, activity.transition_time),
|
||||
prev_activity.expire,
|
||||
timetype=TimeType.REAL,
|
||||
)
|
||||
self._in_set_activity = False
|
||||
|
||||
def getactivity(self) -> ba.Activity | None:
|
||||
|
|
@ -495,15 +529,18 @@ class Session:
|
|||
"""
|
||||
return []
|
||||
|
||||
def _complete_end_activity(self, activity: ba.Activity,
|
||||
results: Any) -> None:
|
||||
def _complete_end_activity(
|
||||
self, activity: ba.Activity, results: Any
|
||||
) -> None:
|
||||
# Run the subclass callback in the session context.
|
||||
try:
|
||||
with _ba.Context(self):
|
||||
self.on_activity_end(activity, results)
|
||||
except Exception:
|
||||
print_exception(f'Error in on_activity_end() for session {self}'
|
||||
f' activity {activity} with results {results}')
|
||||
print_exception(
|
||||
f'Error in on_activity_end() for session {self}'
|
||||
f' activity {activity} with results {results}'
|
||||
)
|
||||
|
||||
def _request_player(self, sessionplayer: ba.SessionPlayer) -> bool:
|
||||
"""Called by the native layer when a player wants to join."""
|
||||
|
|
@ -571,7 +608,8 @@ class Session:
|
|||
if self._activity_should_end_immediately:
|
||||
self._activity_retained.end(
|
||||
self._activity_should_end_immediately_results,
|
||||
self._activity_should_end_immediately_delay)
|
||||
self._activity_should_end_immediately_delay,
|
||||
)
|
||||
|
||||
def _on_player_ready(self, chooser: ba.Chooser) -> None:
|
||||
"""Called when a ba.Player has checked themself ready."""
|
||||
|
|
@ -601,8 +639,10 @@ class Session:
|
|||
self._complete_end_activity(activity, {})
|
||||
else:
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='notEnoughPlayersText',
|
||||
subs=[('${COUNT}', str(min_players))]),
|
||||
Lstr(
|
||||
resource='notEnoughPlayersText',
|
||||
subs=[('${COUNT}', str(min_players))],
|
||||
),
|
||||
color=(1, 1, 0),
|
||||
)
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
|
|
@ -613,7 +653,8 @@ class Session:
|
|||
lobby.remove_chooser(chooser.getplayer())
|
||||
|
||||
def transitioning_out_activity_was_freed(
|
||||
self, can_show_ad_on_death: bool) -> None:
|
||||
self, can_show_ad_on_death: bool
|
||||
) -> None:
|
||||
"""(internal)"""
|
||||
# pylint: disable=cyclic-import
|
||||
from ba._apputils import garbage_collect
|
||||
|
|
@ -632,10 +673,12 @@ class Session:
|
|||
|
||||
def _add_chosen_player(self, chooser: ba.Chooser) -> ba.SessionPlayer:
|
||||
from ba._team import SessionTeam
|
||||
|
||||
sessionplayer = chooser.getplayer()
|
||||
assert sessionplayer in self.sessionplayers, (
|
||||
'SessionPlayer not found in session '
|
||||
'player-list after chooser selection.')
|
||||
'player-list after chooser selection.'
|
||||
)
|
||||
|
||||
activity = self._activity_weak()
|
||||
assert activity is not None
|
||||
|
|
@ -646,20 +689,26 @@ class Session:
|
|||
|
||||
# We can pass it to the current activity if it has already begun
|
||||
# (otherwise it'll get passed once begin is called).
|
||||
pass_to_activity = (activity.has_begun()
|
||||
and not activity.is_joining_activity)
|
||||
pass_to_activity = (
|
||||
activity.has_begun() and not activity.is_joining_activity
|
||||
)
|
||||
|
||||
# However, if we're not allowing mid-game joins, don't actually pass;
|
||||
# just announce the arrival and say they'll partake next round.
|
||||
if pass_to_activity:
|
||||
if not (activity.allow_mid_activity_joins
|
||||
and self.should_allow_mid_activity_joins(activity)):
|
||||
if not (
|
||||
activity.allow_mid_activity_joins
|
||||
and self.should_allow_mid_activity_joins(activity)
|
||||
):
|
||||
pass_to_activity = False
|
||||
with _ba.Context(self):
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='playerDelayedJoinText',
|
||||
subs=[('${PLAYER}',
|
||||
sessionplayer.getname(full=True))]),
|
||||
Lstr(
|
||||
resource='playerDelayedJoinText',
|
||||
subs=[
|
||||
('${PLAYER}', sessionplayer.getname(full=True))
|
||||
],
|
||||
),
|
||||
color=(0, 1, 0),
|
||||
)
|
||||
|
||||
|
|
@ -691,10 +740,12 @@ class Session:
|
|||
|
||||
assert sessionplayer not in sessionteam.players
|
||||
sessionteam.players.append(sessionplayer)
|
||||
sessionplayer.setdata(team=sessionteam,
|
||||
character=chooser.get_character_name(),
|
||||
color=chooser.get_color(),
|
||||
highlight=chooser.get_highlight())
|
||||
sessionplayer.setdata(
|
||||
team=sessionteam,
|
||||
character=chooser.get_character_name(),
|
||||
color=chooser.get_color(),
|
||||
highlight=chooser.get_highlight(),
|
||||
)
|
||||
|
||||
self.stats.register_sessionplayer(sessionplayer)
|
||||
if pass_to_activity:
|
||||
|
|
|
|||
6
dist/ba_data/python/ba/_settings.py
vendored
6
dist/ba_data/python/ba/_settings.py
vendored
|
|
@ -28,6 +28,7 @@ class BoolSetting(Setting):
|
|||
|
||||
Category: Settings Classes
|
||||
"""
|
||||
|
||||
default: bool
|
||||
|
||||
|
||||
|
|
@ -37,6 +38,7 @@ class IntSetting(Setting):
|
|||
|
||||
Category: Settings Classes
|
||||
"""
|
||||
|
||||
default: int
|
||||
min_value: int = 0
|
||||
max_value: int = 9999
|
||||
|
|
@ -49,6 +51,7 @@ class FloatSetting(Setting):
|
|||
|
||||
Category: Settings Classes
|
||||
"""
|
||||
|
||||
default: float
|
||||
min_value: float = 0.0
|
||||
max_value: float = 9999.0
|
||||
|
|
@ -61,6 +64,7 @@ class ChoiceSetting(Setting):
|
|||
|
||||
Category: Settings Classes
|
||||
"""
|
||||
|
||||
choices: list[tuple[str, Any]]
|
||||
|
||||
|
||||
|
|
@ -70,6 +74,7 @@ class IntChoiceSetting(ChoiceSetting):
|
|||
|
||||
Category: Settings Classes
|
||||
"""
|
||||
|
||||
default: int
|
||||
choices: list[tuple[str, int]]
|
||||
|
||||
|
|
@ -80,5 +85,6 @@ class FloatChoiceSetting(ChoiceSetting):
|
|||
|
||||
Category: Settings Classes
|
||||
"""
|
||||
|
||||
default: float
|
||||
choices: list[tuple[str, float]]
|
||||
|
|
|
|||
226
dist/ba_data/python/ba/_stats.py
vendored
226
dist/ba_data/python/ba/_stats.py
vendored
|
|
@ -9,8 +9,13 @@ from typing import TYPE_CHECKING
|
|||
from dataclasses import dataclass
|
||||
|
||||
import _ba
|
||||
from ba._error import (print_exception, print_error, SessionTeamNotFoundError,
|
||||
SessionPlayerNotFoundError, NotFoundError)
|
||||
from ba._error import (
|
||||
print_exception,
|
||||
print_error,
|
||||
SessionTeamNotFoundError,
|
||||
SessionPlayerNotFoundError,
|
||||
NotFoundError,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import ba
|
||||
|
|
@ -37,10 +42,16 @@ class PlayerRecord:
|
|||
still present (stats may be retained for players that leave
|
||||
mid-game)
|
||||
"""
|
||||
|
||||
character: str
|
||||
|
||||
def __init__(self, name: str, name_full: str,
|
||||
sessionplayer: ba.SessionPlayer, 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
|
||||
|
|
@ -105,8 +116,9 @@ class PlayerRecord:
|
|||
return stats.getactivity()
|
||||
return None
|
||||
|
||||
def associate_with_sessionplayer(self,
|
||||
sessionplayer: ba.SessionPlayer) -> None:
|
||||
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
|
||||
|
|
@ -129,6 +141,7 @@ class PlayerRecord:
|
|||
# pylint: disable=too-many-statements
|
||||
from ba._language import Lstr
|
||||
from ba._general import Call
|
||||
|
||||
self._multi_kill_count += 1
|
||||
stats = self._stats()
|
||||
assert stats
|
||||
|
|
@ -169,16 +182,23 @@ class PlayerRecord:
|
|||
sound = stats.orchestrahitsound4
|
||||
else:
|
||||
score = 100
|
||||
name = Lstr(resource='multiKillText',
|
||||
subs=[('${COUNT}', str(self._multi_kill_count))])
|
||||
name = Lstr(
|
||||
resource='multiKillText',
|
||||
subs=[('${COUNT}', str(self._multi_kill_count))],
|
||||
)
|
||||
color = (1.0, 0.5, 0.0, 1)
|
||||
scale = 1.3
|
||||
delay = 1.0
|
||||
sound = stats.orchestrahitsound4
|
||||
|
||||
def _apply(name2: Lstr, score2: int, showpoints2: bool,
|
||||
color2: tuple[float, float, float, float], scale2: float,
|
||||
sound2: ba.Sound | None) -> None:
|
||||
def _apply(
|
||||
name2: Lstr,
|
||||
score2: int,
|
||||
showpoints2: bool,
|
||||
color2: tuple[float, float, float, float],
|
||||
scale2: float,
|
||||
sound2: ba.Sound | None,
|
||||
) -> None:
|
||||
from bastd.actor.popuptext import PopupText
|
||||
|
||||
# Only award this if they're still alive and we can get
|
||||
|
|
@ -194,18 +214,23 @@ class PlayerRecord:
|
|||
return
|
||||
|
||||
# Jitter position a bit since these often come in clusters.
|
||||
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)
|
||||
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(
|
||||
value=(('+' + str(score2) + ' ') if showpoints2 else '') +
|
||||
'${N}',
|
||||
subs=[('${N}', name2)]),
|
||||
color=color2,
|
||||
scale=scale2,
|
||||
position=our_pos).autoretain()
|
||||
PopupText(
|
||||
Lstr(
|
||||
value=(('+' + str(score2) + ' ') if showpoints2 else '')
|
||||
+ '${N}',
|
||||
subs=[('${N}', name2)],
|
||||
),
|
||||
color=color2,
|
||||
scale=scale2,
|
||||
position=our_pos,
|
||||
).autoretain()
|
||||
if sound2:
|
||||
_ba.playsound(sound2)
|
||||
|
||||
|
|
@ -219,7 +244,8 @@ class PlayerRecord:
|
|||
if name is not None:
|
||||
_ba.timer(
|
||||
0.3 + delay,
|
||||
Call(_apply, name, score, showpoints, color, scale, sound))
|
||||
Call(_apply, name, score, showpoints, color, scale, sound),
|
||||
)
|
||||
|
||||
# Keep the tally rollin'...
|
||||
# set a timer for a bit in the future.
|
||||
|
|
@ -296,8 +322,9 @@ class Stats:
|
|||
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)
|
||||
self._player_records[name] = PlayerRecord(
|
||||
name, name_full, player, self
|
||||
)
|
||||
|
||||
def get_records(self) -> dict[str, ba.PlayerRecord]:
|
||||
"""Get PlayerRecord corresponding to still-existing players."""
|
||||
|
|
@ -311,20 +338,22 @@ class Stats:
|
|||
records[record_id] = record
|
||||
return records
|
||||
|
||||
def player_scored(self,
|
||||
player: ba.Player,
|
||||
base_points: int = 1,
|
||||
target: Sequence[float] | None = None,
|
||||
kill: bool = False,
|
||||
victim_player: ba.Player | None = None,
|
||||
scale: float = 1.0,
|
||||
color: Sequence[float] | None = None,
|
||||
title: str | ba.Lstr | None = None,
|
||||
screenmessage: bool = True,
|
||||
display: bool = True,
|
||||
importance: int = 1,
|
||||
showpoints: bool = True,
|
||||
big_message: bool = False) -> int:
|
||||
def player_scored(
|
||||
self,
|
||||
player: ba.Player,
|
||||
base_points: int = 1,
|
||||
target: Sequence[float] | None = None,
|
||||
kill: bool = False,
|
||||
victim_player: ba.Player | None = None,
|
||||
scale: float = 1.0,
|
||||
color: Sequence[float] | None = None,
|
||||
title: str | ba.Lstr | None = None,
|
||||
screenmessage: bool = True,
|
||||
display: bool = True,
|
||||
importance: int = 1,
|
||||
showpoints: bool = True,
|
||||
big_message: bool = False,
|
||||
) -> int:
|
||||
"""Register a score for the player.
|
||||
|
||||
Return value is actual score with multipliers and such factored in.
|
||||
|
|
@ -338,6 +367,7 @@ class Stats:
|
|||
from ba import _math
|
||||
from ba._gameactivity import GameActivity
|
||||
from ba._language import Lstr
|
||||
|
||||
del victim_player # Currently unused.
|
||||
name = player.getname()
|
||||
s_player = self._player_records[name]
|
||||
|
|
@ -361,9 +391,12 @@ class Stats:
|
|||
if isinstance(activity, GameActivity):
|
||||
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))
|
||||
Lstr(
|
||||
resource='nameScoresText',
|
||||
subs=[('${NAME}', name_full)],
|
||||
),
|
||||
color=_math.normalized_color(player.team.color),
|
||||
)
|
||||
except Exception:
|
||||
print_exception('error showing big_message')
|
||||
|
||||
|
|
@ -376,21 +409,26 @@ class Stats:
|
|||
|
||||
# If display-pos is *way* lower than us, raise it up
|
||||
# (so we can still see scores from dudes that fell off cliffs).
|
||||
display_pos = (target[0], max(target[1], our_pos[1] - 2.0),
|
||||
min(target[2], our_pos[2] + 2.0))
|
||||
display_pos = (
|
||||
target[0],
|
||||
max(target[1], our_pos[1] - 2.0),
|
||||
min(target[2], our_pos[2] + 2.0),
|
||||
)
|
||||
activity = self.getactivity()
|
||||
if activity is not None:
|
||||
if title is not None:
|
||||
sval = Lstr(value='+${A} ${B}',
|
||||
subs=[('${A}', str(points)),
|
||||
('${B}', title)])
|
||||
sval = Lstr(
|
||||
value='+${A} ${B}',
|
||||
subs=[('${A}', str(points)), ('${B}', title)],
|
||||
)
|
||||
else:
|
||||
sval = Lstr(value='+${A}',
|
||||
subs=[('${A}', str(points))])
|
||||
PopupText(sval,
|
||||
color=display_color,
|
||||
scale=1.2 * scale,
|
||||
position=display_pos).autoretain()
|
||||
sval = Lstr(value='+${A}', subs=[('${A}', str(points))])
|
||||
PopupText(
|
||||
sval,
|
||||
color=display_color,
|
||||
scale=1.2 * scale,
|
||||
position=display_pos,
|
||||
).autoretain()
|
||||
|
||||
# Tally kills.
|
||||
if kill:
|
||||
|
|
@ -400,11 +438,12 @@ class Stats:
|
|||
# Report non-kill scorings.
|
||||
try:
|
||||
if screenmessage and not kill:
|
||||
_ba.screenmessage(Lstr(resource='nameScoresText',
|
||||
subs=[('${NAME}', name)]),
|
||||
top=True,
|
||||
color=player.color,
|
||||
image=player.get_icon())
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='nameScoresText', subs=[('${NAME}', name)]),
|
||||
top=True,
|
||||
color=player.color,
|
||||
image=player.get_icon(),
|
||||
)
|
||||
except Exception:
|
||||
print_exception('error announcing score')
|
||||
|
||||
|
|
@ -419,12 +458,15 @@ class Stats:
|
|||
|
||||
return points
|
||||
|
||||
def player_was_killed(self,
|
||||
player: ba.Player,
|
||||
killed: bool = False,
|
||||
killer: ba.Player | None = None) -> None:
|
||||
def player_was_killed(
|
||||
self,
|
||||
player: ba.Player,
|
||||
killed: bool = False,
|
||||
killer: ba.Player | None = None,
|
||||
) -> None:
|
||||
"""Should be called when a player is killed."""
|
||||
from ba._language import Lstr
|
||||
|
||||
name = player.getname()
|
||||
prec = self._player_records[name]
|
||||
prec.streak = 0
|
||||
|
|
@ -434,33 +476,47 @@ class Stats:
|
|||
try:
|
||||
if killed and _ba.getactivity().announce_player_deaths:
|
||||
if killer is player:
|
||||
_ba.screenmessage(Lstr(resource='nameSuicideText',
|
||||
subs=[('${NAME}', name)]),
|
||||
top=True,
|
||||
color=player.color,
|
||||
image=player.get_icon())
|
||||
_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 is player.team:
|
||||
_ba.screenmessage(Lstr(resource='nameBetrayedText',
|
||||
subs=[('${NAME}',
|
||||
killer.getname()),
|
||||
('${VICTIM}', name)]),
|
||||
top=True,
|
||||
color=killer.color,
|
||||
image=killer.get_icon())
|
||||
_ba.screenmessage(
|
||||
Lstr(
|
||||
resource='nameBetrayedText',
|
||||
subs=[
|
||||
('${NAME}', killer.getname()),
|
||||
('${VICTIM}', name),
|
||||
],
|
||||
),
|
||||
top=True,
|
||||
color=killer.color,
|
||||
image=killer.get_icon(),
|
||||
)
|
||||
else:
|
||||
_ba.screenmessage(Lstr(resource='nameKilledText',
|
||||
subs=[('${NAME}',
|
||||
killer.getname()),
|
||||
('${VICTIM}', name)]),
|
||||
top=True,
|
||||
color=killer.color,
|
||||
image=killer.get_icon())
|
||||
_ba.screenmessage(
|
||||
Lstr(
|
||||
resource='nameKilledText',
|
||||
subs=[
|
||||
('${NAME}', killer.getname()),
|
||||
('${VICTIM}', name),
|
||||
],
|
||||
),
|
||||
top=True,
|
||||
color=killer.color,
|
||||
image=killer.get_icon(),
|
||||
)
|
||||
else:
|
||||
_ba.screenmessage(Lstr(resource='nameDiedText',
|
||||
subs=[('${NAME}', name)]),
|
||||
top=True,
|
||||
color=player.color,
|
||||
image=player.get_icon())
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='nameDiedText', subs=[('${NAME}', name)]),
|
||||
top=True,
|
||||
color=player.color,
|
||||
image=player.get_icon(),
|
||||
)
|
||||
except Exception:
|
||||
print_exception('error announcing kill')
|
||||
|
|
|
|||
427
dist/ba_data/python/ba/_store.py
vendored
427
dist/ba_data/python/ba/_store.py
vendored
|
|
@ -24,14 +24,17 @@ def get_store_item_name_translated(item_name: str) -> ba.Lstr:
|
|||
# pylint: disable=cyclic-import
|
||||
from ba import _language
|
||||
from ba import _map
|
||||
|
||||
item_info = get_store_item(item_name)
|
||||
if item_name.startswith('characters.'):
|
||||
return _language.Lstr(translate=('characterNames',
|
||||
item_info['character']))
|
||||
return _language.Lstr(
|
||||
translate=('characterNames', item_info['character'])
|
||||
)
|
||||
if item_name in ['upgrades.pro', 'pro']:
|
||||
return _language.Lstr(resource='store.bombSquadProNameText',
|
||||
subs=[('${APP_NAME}',
|
||||
_language.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)
|
||||
|
|
@ -64,6 +67,7 @@ def get_store_items() -> dict[str, dict]:
|
|||
# pylint: disable=cyclic-import
|
||||
from ba._generated.enums import SpecialChar
|
||||
from bastd import maps
|
||||
|
||||
if _ba.app.store_items is None:
|
||||
from bastd.game import ninjafight
|
||||
from bastd.game import meteorshower
|
||||
|
|
@ -73,114 +77,64 @@ def get_store_items() -> dict[str, dict]:
|
|||
# IMPORTANT - need to keep this synced with the master server.
|
||||
# (doing so manually for now)
|
||||
_ba.app.store_items = {
|
||||
'characters.kronk': {
|
||||
'character': 'Kronk'
|
||||
},
|
||||
'characters.zoe': {
|
||||
'character': 'Zoe'
|
||||
},
|
||||
'characters.jackmorgan': {
|
||||
'character': 'Jack Morgan'
|
||||
},
|
||||
'characters.mel': {
|
||||
'character': 'Mel'
|
||||
},
|
||||
'characters.snakeshadow': {
|
||||
'character': 'Snake Shadow'
|
||||
},
|
||||
'characters.bones': {
|
||||
'character': 'Bones'
|
||||
},
|
||||
'characters.kronk': {'character': 'Kronk'},
|
||||
'characters.zoe': {'character': 'Zoe'},
|
||||
'characters.jackmorgan': {'character': 'Jack Morgan'},
|
||||
'characters.mel': {'character': 'Mel'},
|
||||
'characters.snakeshadow': {'character': 'Snake Shadow'},
|
||||
'characters.bones': {'character': 'Bones'},
|
||||
'characters.bernard': {
|
||||
'character': 'Bernard',
|
||||
'highlight': (0.6, 0.5, 0.8)
|
||||
},
|
||||
'characters.pixie': {
|
||||
'character': 'Pixel'
|
||||
},
|
||||
'characters.wizard': {
|
||||
'character': 'Grumbledorf'
|
||||
},
|
||||
'characters.frosty': {
|
||||
'character': 'Frosty'
|
||||
},
|
||||
'characters.pascal': {
|
||||
'character': 'Pascal'
|
||||
},
|
||||
'characters.cyborg': {
|
||||
'character': 'B-9000'
|
||||
},
|
||||
'characters.agent': {
|
||||
'character': 'Agent Johnson'
|
||||
},
|
||||
'characters.taobaomascot': {
|
||||
'character': 'Taobao Mascot'
|
||||
},
|
||||
'characters.santa': {
|
||||
'character': 'Santa Claus'
|
||||
},
|
||||
'characters.bunny': {
|
||||
'character': 'Easter Bunny'
|
||||
'highlight': (0.6, 0.5, 0.8),
|
||||
},
|
||||
'characters.pixie': {'character': 'Pixel'},
|
||||
'characters.wizard': {'character': 'Grumbledorf'},
|
||||
'characters.frosty': {'character': 'Frosty'},
|
||||
'characters.pascal': {'character': 'Pascal'},
|
||||
'characters.cyborg': {'character': 'B-9000'},
|
||||
'characters.agent': {'character': 'Agent Johnson'},
|
||||
'characters.taobaomascot': {'character': 'Taobao Mascot'},
|
||||
'characters.santa': {'character': 'Santa Claus'},
|
||||
'characters.bunny': {'character': 'Easter Bunny'},
|
||||
'pro': {},
|
||||
'maps.lake_frigid': {
|
||||
'map_type': maps.LakeFrigid
|
||||
},
|
||||
'maps.lake_frigid': {'map_type': maps.LakeFrigid},
|
||||
'games.ninja_fight': {
|
||||
'gametype': ninjafight.NinjaFightGame,
|
||||
'previewTex': 'courtyardPreview'
|
||||
'previewTex': 'courtyardPreview',
|
||||
},
|
||||
'games.meteor_shower': {
|
||||
'gametype': meteorshower.MeteorShowerGame,
|
||||
'previewTex': 'rampagePreview'
|
||||
'previewTex': 'rampagePreview',
|
||||
},
|
||||
'games.target_practice': {
|
||||
'gametype': targetpractice.TargetPracticeGame,
|
||||
'previewTex': 'doomShroomPreview'
|
||||
'previewTex': 'doomShroomPreview',
|
||||
},
|
||||
'games.easter_egg_hunt': {
|
||||
'gametype': easteregghunt.EasterEggHuntGame,
|
||||
'previewTex': 'towerDPreview'
|
||||
'previewTex': 'towerDPreview',
|
||||
},
|
||||
'icons.flag_us': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_UNITED_STATES)
|
||||
},
|
||||
'icons.flag_mexico': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_MEXICO)
|
||||
},
|
||||
'icons.flag_mexico': {'icon': _ba.charstr(SpecialChar.FLAG_MEXICO)},
|
||||
'icons.flag_germany': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_GERMANY)
|
||||
},
|
||||
'icons.flag_brazil': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_BRAZIL)
|
||||
},
|
||||
'icons.flag_russia': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_RUSSIA)
|
||||
},
|
||||
'icons.flag_china': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_CHINA)
|
||||
},
|
||||
'icons.flag_brazil': {'icon': _ba.charstr(SpecialChar.FLAG_BRAZIL)},
|
||||
'icons.flag_russia': {'icon': _ba.charstr(SpecialChar.FLAG_RUSSIA)},
|
||||
'icons.flag_china': {'icon': _ba.charstr(SpecialChar.FLAG_CHINA)},
|
||||
'icons.flag_uk': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_UNITED_KINGDOM)
|
||||
},
|
||||
'icons.flag_canada': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_CANADA)
|
||||
},
|
||||
'icons.flag_india': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_INDIA)
|
||||
},
|
||||
'icons.flag_japan': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_JAPAN)
|
||||
},
|
||||
'icons.flag_france': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_FRANCE)
|
||||
},
|
||||
'icons.flag_canada': {'icon': _ba.charstr(SpecialChar.FLAG_CANADA)},
|
||||
'icons.flag_india': {'icon': _ba.charstr(SpecialChar.FLAG_INDIA)},
|
||||
'icons.flag_japan': {'icon': _ba.charstr(SpecialChar.FLAG_JAPAN)},
|
||||
'icons.flag_france': {'icon': _ba.charstr(SpecialChar.FLAG_FRANCE)},
|
||||
'icons.flag_indonesia': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_INDONESIA)
|
||||
},
|
||||
'icons.flag_italy': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_ITALY)
|
||||
},
|
||||
'icons.flag_italy': {'icon': _ba.charstr(SpecialChar.FLAG_ITALY)},
|
||||
'icons.flag_south_korea': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_SOUTH_KOREA)
|
||||
},
|
||||
|
|
@ -190,15 +144,9 @@ def get_store_items() -> dict[str, dict]:
|
|||
'icons.flag_uae': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_UNITED_ARAB_EMIRATES)
|
||||
},
|
||||
'icons.flag_qatar': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_QATAR)
|
||||
},
|
||||
'icons.flag_egypt': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_EGYPT)
|
||||
},
|
||||
'icons.flag_kuwait': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_KUWAIT)
|
||||
},
|
||||
'icons.flag_qatar': {'icon': _ba.charstr(SpecialChar.FLAG_QATAR)},
|
||||
'icons.flag_egypt': {'icon': _ba.charstr(SpecialChar.FLAG_EGYPT)},
|
||||
'icons.flag_kuwait': {'icon': _ba.charstr(SpecialChar.FLAG_KUWAIT)},
|
||||
'icons.flag_algeria': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_ALGERIA)
|
||||
},
|
||||
|
|
@ -217,69 +165,33 @@ def get_store_items() -> dict[str, dict]:
|
|||
'icons.flag_singapore': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_SINGAPORE)
|
||||
},
|
||||
'icons.flag_iran': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_IRAN)
|
||||
},
|
||||
'icons.flag_poland': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_POLAND)
|
||||
},
|
||||
'icons.flag_iran': {'icon': _ba.charstr(SpecialChar.FLAG_IRAN)},
|
||||
'icons.flag_poland': {'icon': _ba.charstr(SpecialChar.FLAG_POLAND)},
|
||||
'icons.flag_argentina': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_ARGENTINA)
|
||||
},
|
||||
'icons.flag_philippines': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_PHILIPPINES)
|
||||
},
|
||||
'icons.flag_chile': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_CHILE)
|
||||
},
|
||||
'icons.fedora': {
|
||||
'icon': _ba.charstr(SpecialChar.FEDORA)
|
||||
},
|
||||
'icons.hal': {
|
||||
'icon': _ba.charstr(SpecialChar.HAL)
|
||||
},
|
||||
'icons.crown': {
|
||||
'icon': _ba.charstr(SpecialChar.CROWN)
|
||||
},
|
||||
'icons.yinyang': {
|
||||
'icon': _ba.charstr(SpecialChar.YIN_YANG)
|
||||
},
|
||||
'icons.eyeball': {
|
||||
'icon': _ba.charstr(SpecialChar.EYE_BALL)
|
||||
},
|
||||
'icons.skull': {
|
||||
'icon': _ba.charstr(SpecialChar.SKULL)
|
||||
},
|
||||
'icons.heart': {
|
||||
'icon': _ba.charstr(SpecialChar.HEART)
|
||||
},
|
||||
'icons.dragon': {
|
||||
'icon': _ba.charstr(SpecialChar.DRAGON)
|
||||
},
|
||||
'icons.helmet': {
|
||||
'icon': _ba.charstr(SpecialChar.HELMET)
|
||||
},
|
||||
'icons.mushroom': {
|
||||
'icon': _ba.charstr(SpecialChar.MUSHROOM)
|
||||
},
|
||||
'icons.ninja_star': {
|
||||
'icon': _ba.charstr(SpecialChar.NINJA_STAR)
|
||||
},
|
||||
'icons.flag_chile': {'icon': _ba.charstr(SpecialChar.FLAG_CHILE)},
|
||||
'icons.fedora': {'icon': _ba.charstr(SpecialChar.FEDORA)},
|
||||
'icons.hal': {'icon': _ba.charstr(SpecialChar.HAL)},
|
||||
'icons.crown': {'icon': _ba.charstr(SpecialChar.CROWN)},
|
||||
'icons.yinyang': {'icon': _ba.charstr(SpecialChar.YIN_YANG)},
|
||||
'icons.eyeball': {'icon': _ba.charstr(SpecialChar.EYE_BALL)},
|
||||
'icons.skull': {'icon': _ba.charstr(SpecialChar.SKULL)},
|
||||
'icons.heart': {'icon': _ba.charstr(SpecialChar.HEART)},
|
||||
'icons.dragon': {'icon': _ba.charstr(SpecialChar.DRAGON)},
|
||||
'icons.helmet': {'icon': _ba.charstr(SpecialChar.HELMET)},
|
||||
'icons.mushroom': {'icon': _ba.charstr(SpecialChar.MUSHROOM)},
|
||||
'icons.ninja_star': {'icon': _ba.charstr(SpecialChar.NINJA_STAR)},
|
||||
'icons.viking_helmet': {
|
||||
'icon': _ba.charstr(SpecialChar.VIKING_HELMET)
|
||||
},
|
||||
'icons.moon': {
|
||||
'icon': _ba.charstr(SpecialChar.MOON)
|
||||
},
|
||||
'icons.spider': {
|
||||
'icon': _ba.charstr(SpecialChar.SPIDER)
|
||||
},
|
||||
'icons.fireball': {
|
||||
'icon': _ba.charstr(SpecialChar.FIREBALL)
|
||||
},
|
||||
'icons.mikirog': {
|
||||
'icon': _ba.charstr(SpecialChar.MIKIROG)
|
||||
},
|
||||
'icons.moon': {'icon': _ba.charstr(SpecialChar.MOON)},
|
||||
'icons.spider': {'icon': _ba.charstr(SpecialChar.SPIDER)},
|
||||
'icons.fireball': {'icon': _ba.charstr(SpecialChar.FIREBALL)},
|
||||
'icons.mikirog': {'icon': _ba.charstr(SpecialChar.MIKIROG)},
|
||||
}
|
||||
store_items = _ba.app.store_items
|
||||
assert store_items is not None
|
||||
|
|
@ -289,97 +201,107 @@ def get_store_items() -> dict[str, dict]:
|
|||
def get_store_layout() -> dict[str, list[dict[str, Any]]]:
|
||||
"""Return what's available in the store at a given time.
|
||||
|
||||
Categorized by tab and by section."""
|
||||
Categorized by tab and by section."""
|
||||
if _ba.app.store_layout is None:
|
||||
_ba.app.store_layout = {
|
||||
'characters': [{
|
||||
'items': []
|
||||
}],
|
||||
'extras': [{
|
||||
'items': ['pro']
|
||||
}],
|
||||
'maps': [{
|
||||
'items': ['maps.lake_frigid']
|
||||
}],
|
||||
'characters': [{'items': []}],
|
||||
'extras': [{'items': ['pro']}],
|
||||
'maps': [{'items': ['maps.lake_frigid']}],
|
||||
'minigames': [],
|
||||
'icons': [{
|
||||
'items': [
|
||||
'icons.mushroom',
|
||||
'icons.heart',
|
||||
'icons.eyeball',
|
||||
'icons.yinyang',
|
||||
'icons.hal',
|
||||
'icons.flag_us',
|
||||
'icons.flag_mexico',
|
||||
'icons.flag_germany',
|
||||
'icons.flag_brazil',
|
||||
'icons.flag_russia',
|
||||
'icons.flag_china',
|
||||
'icons.flag_uk',
|
||||
'icons.flag_canada',
|
||||
'icons.flag_india',
|
||||
'icons.flag_japan',
|
||||
'icons.flag_france',
|
||||
'icons.flag_indonesia',
|
||||
'icons.flag_italy',
|
||||
'icons.flag_south_korea',
|
||||
'icons.flag_netherlands',
|
||||
'icons.flag_uae',
|
||||
'icons.flag_qatar',
|
||||
'icons.flag_egypt',
|
||||
'icons.flag_kuwait',
|
||||
'icons.flag_algeria',
|
||||
'icons.flag_saudi_arabia',
|
||||
'icons.flag_malaysia',
|
||||
'icons.flag_czech_republic',
|
||||
'icons.flag_australia',
|
||||
'icons.flag_singapore',
|
||||
'icons.flag_iran',
|
||||
'icons.flag_poland',
|
||||
'icons.flag_argentina',
|
||||
'icons.flag_philippines',
|
||||
'icons.flag_chile',
|
||||
'icons.moon',
|
||||
'icons.fedora',
|
||||
'icons.spider',
|
||||
'icons.ninja_star',
|
||||
'icons.skull',
|
||||
'icons.dragon',
|
||||
'icons.viking_helmet',
|
||||
'icons.fireball',
|
||||
'icons.helmet',
|
||||
'icons.crown',
|
||||
]
|
||||
}]
|
||||
'icons': [
|
||||
{
|
||||
'items': [
|
||||
'icons.mushroom',
|
||||
'icons.heart',
|
||||
'icons.eyeball',
|
||||
'icons.yinyang',
|
||||
'icons.hal',
|
||||
'icons.flag_us',
|
||||
'icons.flag_mexico',
|
||||
'icons.flag_germany',
|
||||
'icons.flag_brazil',
|
||||
'icons.flag_russia',
|
||||
'icons.flag_china',
|
||||
'icons.flag_uk',
|
||||
'icons.flag_canada',
|
||||
'icons.flag_india',
|
||||
'icons.flag_japan',
|
||||
'icons.flag_france',
|
||||
'icons.flag_indonesia',
|
||||
'icons.flag_italy',
|
||||
'icons.flag_south_korea',
|
||||
'icons.flag_netherlands',
|
||||
'icons.flag_uae',
|
||||
'icons.flag_qatar',
|
||||
'icons.flag_egypt',
|
||||
'icons.flag_kuwait',
|
||||
'icons.flag_algeria',
|
||||
'icons.flag_saudi_arabia',
|
||||
'icons.flag_malaysia',
|
||||
'icons.flag_czech_republic',
|
||||
'icons.flag_australia',
|
||||
'icons.flag_singapore',
|
||||
'icons.flag_iran',
|
||||
'icons.flag_poland',
|
||||
'icons.flag_argentina',
|
||||
'icons.flag_philippines',
|
||||
'icons.flag_chile',
|
||||
'icons.moon',
|
||||
'icons.fedora',
|
||||
'icons.spider',
|
||||
'icons.ninja_star',
|
||||
'icons.skull',
|
||||
'icons.dragon',
|
||||
'icons.viking_helmet',
|
||||
'icons.fireball',
|
||||
'icons.helmet',
|
||||
'icons.crown',
|
||||
]
|
||||
}
|
||||
],
|
||||
}
|
||||
store_layout = _ba.app.store_layout
|
||||
assert store_layout is not None
|
||||
store_layout['characters'] = [{
|
||||
'items': [
|
||||
'characters.kronk', 'characters.zoe', 'characters.jackmorgan',
|
||||
'characters.mel', 'characters.snakeshadow', 'characters.bones',
|
||||
'characters.bernard', 'characters.agent', 'characters.frosty',
|
||||
'characters.pascal', 'characters.pixie'
|
||||
]
|
||||
}]
|
||||
store_layout['minigames'] = [{
|
||||
'items': [
|
||||
'games.ninja_fight', 'games.meteor_shower', 'games.target_practice'
|
||||
]
|
||||
}]
|
||||
store_layout['characters'] = [
|
||||
{
|
||||
'items': [
|
||||
'characters.kronk',
|
||||
'characters.zoe',
|
||||
'characters.jackmorgan',
|
||||
'characters.mel',
|
||||
'characters.snakeshadow',
|
||||
'characters.bones',
|
||||
'characters.bernard',
|
||||
'characters.agent',
|
||||
'characters.frosty',
|
||||
'characters.pascal',
|
||||
'characters.pixie',
|
||||
]
|
||||
}
|
||||
]
|
||||
store_layout['minigames'] = [
|
||||
{
|
||||
'items': [
|
||||
'games.ninja_fight',
|
||||
'games.meteor_shower',
|
||||
'games.target_practice',
|
||||
]
|
||||
}
|
||||
]
|
||||
if _internal.get_v1_account_misc_read_val('xmas', False):
|
||||
store_layout['characters'][0]['items'].append('characters.santa')
|
||||
store_layout['characters'][0]['items'].append('characters.wizard')
|
||||
store_layout['characters'][0]['items'].append('characters.cyborg')
|
||||
if _internal.get_v1_account_misc_read_val('easter', False):
|
||||
store_layout['characters'].append({
|
||||
'title': 'store.holidaySpecialText',
|
||||
'items': ['characters.bunny']
|
||||
})
|
||||
store_layout['minigames'].append({
|
||||
'title': 'store.holidaySpecialText',
|
||||
'items': ['games.easter_egg_hunt']
|
||||
})
|
||||
store_layout['characters'].append(
|
||||
{'title': 'store.holidaySpecialText', 'items': ['characters.bunny']}
|
||||
)
|
||||
store_layout['minigames'].append(
|
||||
{
|
||||
'title': 'store.holidaySpecialText',
|
||||
'items': ['games.easter_egg_hunt'],
|
||||
}
|
||||
)
|
||||
return store_layout
|
||||
|
||||
|
||||
|
|
@ -394,7 +316,7 @@ def get_clean_price(price_string: str) -> str:
|
|||
'$4.99': '$5.00',
|
||||
'$9.99': '$10.00',
|
||||
'$19.99': '$20.00',
|
||||
'$49.99': '$50.00'
|
||||
'$49.99': '$50.00',
|
||||
}
|
||||
return psubs.get(price_string, price_string)
|
||||
|
||||
|
|
@ -418,19 +340,23 @@ def get_available_purchase_count(tab: str | None = None) -> int:
|
|||
return count
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception('error calcing available purchases')
|
||||
return 0
|
||||
|
||||
|
||||
def _calc_count_for_tab(tabval: list[dict[str, Any]], our_tickets: int,
|
||||
count: int) -> int:
|
||||
def _calc_count_for_tab(
|
||||
tabval: list[dict[str, Any]], our_tickets: int, count: int
|
||||
) -> int:
|
||||
for section in tabval:
|
||||
for item in section['items']:
|
||||
ticket_cost = _internal.get_v1_account_misc_read_val(
|
||||
'price.' + item, None)
|
||||
'price.' + item, None
|
||||
)
|
||||
if ticket_cost is not None:
|
||||
if (our_tickets >= ticket_cost
|
||||
and not _internal.get_purchased(item)):
|
||||
if our_tickets >= ticket_cost and not _internal.get_purchased(
|
||||
item
|
||||
):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
|
@ -443,6 +369,7 @@ def get_available_sale_time(tab: str) -> int | None:
|
|||
try:
|
||||
import datetime
|
||||
from ba._generated.enums import TimeType, TimeFormat
|
||||
|
||||
app = _ba.app
|
||||
sale_times: list[int | None] = []
|
||||
|
||||
|
|
@ -458,18 +385,21 @@ def get_available_sale_time(tab: str) -> int | None:
|
|||
# If we've got a time-remaining in our config, start there.
|
||||
if 'PSTR' in config:
|
||||
app.pro_sale_start_time = int(
|
||||
_ba.time(TimeType.REAL, TimeFormat.MILLISECONDS))
|
||||
_ba.time(TimeType.REAL, TimeFormat.MILLISECONDS)
|
||||
)
|
||||
app.pro_sale_start_val = config['PSTR']
|
||||
else:
|
||||
|
||||
# We start the timer once we get the duration from
|
||||
# the server.
|
||||
start_duration = _internal.get_v1_account_misc_read_val(
|
||||
'proSaleDurationMinutes', None)
|
||||
'proSaleDurationMinutes', None
|
||||
)
|
||||
if start_duration is not None:
|
||||
app.pro_sale_start_time = int(
|
||||
_ba.time(TimeType.REAL, TimeFormat.MILLISECONDS))
|
||||
app.pro_sale_start_val = (60000 * start_duration)
|
||||
_ba.time(TimeType.REAL, TimeFormat.MILLISECONDS)
|
||||
)
|
||||
app.pro_sale_start_val = 60000 * start_duration
|
||||
|
||||
# If we haven't heard from the server yet, no sale..
|
||||
else:
|
||||
|
|
@ -477,9 +407,13 @@ def get_available_sale_time(tab: str) -> int | None:
|
|||
|
||||
assert app.pro_sale_start_val is not None
|
||||
val: int | None = max(
|
||||
0, app.pro_sale_start_val -
|
||||
(_ba.time(TimeType.REAL, TimeFormat.MILLISECONDS) -
|
||||
app.pro_sale_start_time))
|
||||
0,
|
||||
app.pro_sale_start_val
|
||||
- (
|
||||
_ba.time(TimeType.REAL, TimeFormat.MILLISECONDS)
|
||||
- app.pro_sale_start_time
|
||||
),
|
||||
)
|
||||
|
||||
# Keep the value in the config up to date. I suppose we should
|
||||
# write the config occasionally but it should happen often enough
|
||||
|
|
@ -496,9 +430,12 @@ def get_available_sale_time(tab: str) -> int | None:
|
|||
for item in section['items']:
|
||||
if item in sales_raw:
|
||||
if not _internal.get_purchased(item):
|
||||
to_end = ((datetime.datetime.utcfromtimestamp(
|
||||
sales_raw[item]['e']) -
|
||||
datetime.datetime.utcnow()).total_seconds())
|
||||
to_end = (
|
||||
datetime.datetime.utcfromtimestamp(
|
||||
sales_raw[item]['e']
|
||||
)
|
||||
- datetime.datetime.utcnow()
|
||||
).total_seconds()
|
||||
if to_end > 0:
|
||||
sale_times.append(int(to_end * 1000))
|
||||
|
||||
|
|
@ -508,6 +445,7 @@ def get_available_sale_time(tab: str) -> int | None:
|
|||
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception('error calcing sale time')
|
||||
return None
|
||||
|
||||
|
|
@ -540,5 +478,6 @@ def get_unowned_game_types() -> set[type[ba.GameActivity]]:
|
|||
return unowned_games
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception('error calcing un-owned games')
|
||||
return set()
|
||||
|
|
|
|||
19
dist/ba_data/python/ba/_team.py
vendored
19
dist/ba_data/python/ba/_team.py
vendored
|
|
@ -44,10 +44,12 @@ class SessionTeam:
|
|||
id: int
|
||||
"""The unique numeric id of the team."""
|
||||
|
||||
def __init__(self,
|
||||
team_id: int = 0,
|
||||
name: ba.Lstr | str = '',
|
||||
color: Sequence[float] = (1.0, 1.0, 1.0)):
|
||||
def __init__(
|
||||
self,
|
||||
team_id: int = 0,
|
||||
name: ba.Lstr | str = '',
|
||||
color: Sequence[float] = (1.0, 1.0, 1.0),
|
||||
):
|
||||
"""Instantiate a ba.SessionTeam.
|
||||
|
||||
In most cases, all teams are provided to you by the ba.Session,
|
||||
|
|
@ -109,7 +111,8 @@ class Team(Generic[PlayerType]):
|
|||
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.')
|
||||
f' in the class decorator.'
|
||||
)
|
||||
|
||||
self.players = []
|
||||
self._sessionteam = weakref.ref(sessionteam)
|
||||
|
|
@ -120,8 +123,9 @@ class Team(Generic[PlayerType]):
|
|||
self._expired = False
|
||||
self._postinited = True
|
||||
|
||||
def manual_init(self, team_id: int, name: ba.Lstr | str,
|
||||
color: tuple[float, ...]) -> None:
|
||||
def manual_init(
|
||||
self, team_id: int, name: ba.Lstr | str, color: tuple[float, ...]
|
||||
) -> None:
|
||||
"""Manually init a team for uses such as bots."""
|
||||
self.id = team_id
|
||||
self.name = name
|
||||
|
|
@ -186,6 +190,7 @@ class Team(Generic[PlayerType]):
|
|||
if sessionteam is not None:
|
||||
return sessionteam
|
||||
from ba import _error
|
||||
|
||||
raise _error.SessionTeamNotFoundError()
|
||||
|
||||
|
||||
|
|
|
|||
53
dist/ba_data/python/ba/_teamgame.py
vendored
53
dist/ba_data/python/ba/_teamgame.py
vendored
|
|
@ -39,8 +39,9 @@ class TeamGameActivity(GameActivity[PlayerType, TeamType]):
|
|||
returns True for ba.DualTeamSessions and ba.FreeForAllSessions;
|
||||
False otherwise.
|
||||
"""
|
||||
return (issubclass(sessiontype, DualTeamSession)
|
||||
or issubclass(sessiontype, FreeForAllSession))
|
||||
return issubclass(sessiontype, DualTeamSession) or issubclass(
|
||||
sessiontype, FreeForAllSession
|
||||
)
|
||||
|
||||
def __init__(self, settings: dict):
|
||||
super().__init__(settings)
|
||||
|
|
@ -55,6 +56,7 @@ class TeamGameActivity(GameActivity[PlayerType, TeamType]):
|
|||
# pylint: disable=cyclic-import
|
||||
from ba._coopsession import CoopSession
|
||||
from bastd.actor.controlsguide import ControlsGuide
|
||||
|
||||
super().on_transition_in()
|
||||
|
||||
# On the first game, show the controls UI momentarily.
|
||||
|
|
@ -67,11 +69,13 @@ class TeamGameActivity(GameActivity[PlayerType, TeamType]):
|
|||
lifespan = 10.0
|
||||
if self.slow_motion:
|
||||
lifespan *= 0.3
|
||||
ControlsGuide(delay=delay,
|
||||
lifespan=lifespan,
|
||||
scale=0.8,
|
||||
position=(380, 200),
|
||||
bright=True).autoretain()
|
||||
ControlsGuide(
|
||||
delay=delay,
|
||||
lifespan=lifespan,
|
||||
scale=0.8,
|
||||
position=(380, 200),
|
||||
bright=True,
|
||||
).autoretain()
|
||||
setattr(self.session, attrname, True)
|
||||
|
||||
def on_begin(self) -> None:
|
||||
|
|
@ -84,15 +88,19 @@ class TeamGameActivity(GameActivity[PlayerType, TeamType]):
|
|||
elif isinstance(self.session, DualTeamSession):
|
||||
if len(self.players) >= 4:
|
||||
from ba import _achievement
|
||||
|
||||
_ba.app.ach.award_local_achievement('Team Player')
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception()
|
||||
|
||||
def spawn_player_spaz(self,
|
||||
player: PlayerType,
|
||||
position: Sequence[float] | None = None,
|
||||
angle: float | None = None) -> PlayerSpaz:
|
||||
def spawn_player_spaz(
|
||||
self,
|
||||
player: PlayerType,
|
||||
position: Sequence[float] | None = None,
|
||||
angle: float | None = None,
|
||||
) -> PlayerSpaz:
|
||||
"""
|
||||
Method override; spawns and wires up a standard ba.PlayerSpaz for
|
||||
a ba.Player.
|
||||
|
|
@ -103,7 +111,7 @@ class TeamGameActivity(GameActivity[PlayerType, TeamType]):
|
|||
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.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)
|
||||
|
|
@ -112,11 +120,12 @@ class TeamGameActivity(GameActivity[PlayerType, TeamType]):
|
|||
|
||||
# FIXME: need to unify these arguments with GameActivity.end()
|
||||
def end( # type: ignore
|
||||
self,
|
||||
results: Any = None,
|
||||
announce_winning_team: bool = True,
|
||||
announce_delay: float = 0.1,
|
||||
force: bool = False) -> None:
|
||||
self,
|
||||
results: Any = None,
|
||||
announce_winning_team: bool = True,
|
||||
announce_delay: float = 0.1,
|
||||
force: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
End the game and announce the single winning team
|
||||
unless 'announce_winning_team' is False.
|
||||
|
|
@ -141,15 +150,19 @@ class TeamGameActivity(GameActivity[PlayerType, TeamType]):
|
|||
self,
|
||||
results,
|
||||
delay=announce_delay,
|
||||
announce_winning_team=announce_winning_team)
|
||||
announce_winning_team=announce_winning_team,
|
||||
)
|
||||
|
||||
# For co-op we just pass this up the chain with a delay added
|
||||
# (in most cases). Team games expect a delay for the announce
|
||||
# portion in teams/ffa mode so this keeps it consistent.
|
||||
else:
|
||||
# don't want delay on restarts..
|
||||
if (isinstance(results, dict) and 'outcome' in results
|
||||
and results['outcome'] == 'restart'):
|
||||
if (
|
||||
isinstance(results, dict)
|
||||
and 'outcome' in results
|
||||
and results['outcome'] == 'restart'
|
||||
):
|
||||
delay = 0.0
|
||||
else:
|
||||
delay = 2.0
|
||||
|
|
|
|||
138
dist/ba_data/python/ba/_tips.py
vendored
138
dist/ba_data/python/ba/_tips.py
vendored
|
|
@ -27,53 +27,94 @@ def get_next_tip() -> str:
|
|||
def get_all_tips() -> list[str]:
|
||||
"""Return the complete list of tips."""
|
||||
tips = [
|
||||
('If you are short on controllers, install the \'${REMOTE_APP_NAME}\' '
|
||||
'app\non your mobile devices to use them as controllers.'),
|
||||
('Create player profiles for yourself and your friends with\nyour '
|
||||
'preferred names and appearances instead of using random ones.'),
|
||||
('You can \'aim\' your punches by spinning left or right.\nThis is '
|
||||
'useful for knocking bad guys off edges or scoring in hockey.'),
|
||||
('If you pick up a curse, your only hope for survival is to\nfind a '
|
||||
'health powerup in the next few seconds.'),
|
||||
('A perfectly timed running-jumping-spin-punch can kill in a single '
|
||||
'hit\nand earn you lifelong respect from your friends.'),
|
||||
(
|
||||
'If you are short on controllers,'
|
||||
' install the \'${REMOTE_APP_NAME}\' app\n'
|
||||
'on your mobile devices to use them as controllers.'
|
||||
),
|
||||
(
|
||||
'Create player profiles for yourself and your friends with\nyour '
|
||||
'preferred names and appearances instead of using random ones.'
|
||||
),
|
||||
(
|
||||
'You can \'aim\' your punches by spinning left or right.\nThis is '
|
||||
'useful for knocking bad guys off edges or scoring in hockey.'
|
||||
),
|
||||
(
|
||||
'If you pick up a curse, your only hope for survival is to\nfind a '
|
||||
'health powerup in the next few seconds.'
|
||||
),
|
||||
(
|
||||
'A perfectly timed running-jumping-spin-punch can kill in a single '
|
||||
'hit\nand earn you lifelong respect from your friends.'
|
||||
),
|
||||
'Always remember to floss.',
|
||||
'Don\'t run all the time. Really. You will fall off cliffs.',
|
||||
('In Capture-the-Flag, your own flag must be at your base to score, '
|
||||
'If the other\nteam is about to score, stealing their flag can be '
|
||||
'a good way to stop them.'),
|
||||
('If you get a sticky-bomb stuck to you, jump around and spin in '
|
||||
'circles. You might\nshake the bomb off, or if nothing else your '
|
||||
'last moments will be entertaining.'),
|
||||
('You take damage when you whack your head on things,\nso try to not '
|
||||
'whack your head on things.'),
|
||||
(
|
||||
'In Capture-the-Flag, your own flag must be at your base to score, '
|
||||
'If the other\nteam is about to score, stealing their flag can be '
|
||||
'a good way to stop them.'
|
||||
),
|
||||
(
|
||||
'If you get a sticky-bomb stuck to you, jump around and spin in '
|
||||
'circles. You might\nshake the bomb off, or if nothing else your '
|
||||
'last moments will be entertaining.'
|
||||
),
|
||||
(
|
||||
'You take damage when you whack your head on things,\n'
|
||||
'so try to not whack your head on things.'
|
||||
),
|
||||
'If you kill an enemy in one hit you get double points for it.',
|
||||
('Despite their looks, all characters\' abilities are identical,\nso '
|
||||
'just pick whichever one you most closely resemble.'),
|
||||
(
|
||||
'Despite their looks, all characters\' abilities are identical,\n'
|
||||
'so just pick whichever one you most closely resemble.'
|
||||
),
|
||||
'You can throw bombs higher if you jump just before throwing.',
|
||||
('Throw strength is based on the direction you are holding.\nTo toss '
|
||||
'something gently in front of you, don\'t hold any direction.'),
|
||||
('If someone picks you up, punch them and they\'ll let go.\nThis '
|
||||
'works in real life too.'),
|
||||
('Don\'t get too cocky with that energy shield; you can still get '
|
||||
'yourself thrown off a cliff.'),
|
||||
('Many things can be picked up and thrown, including other players. '
|
||||
'Tossing\nyour enemies off cliffs can be an effective and '
|
||||
'emotionally fulfilling strategy.'),
|
||||
('Ice bombs are not very powerful, but they freeze\nwhoever they '
|
||||
'hit, leaving them vulnerable to shattering.'),
|
||||
(
|
||||
'Throw strength is based on the direction you are holding.\n'
|
||||
'To toss something gently in front of you, don\'t'
|
||||
' hold any direction.'
|
||||
),
|
||||
(
|
||||
'If someone picks you up, punch them and they\'ll let go.\nThis '
|
||||
'works in real life too.'
|
||||
),
|
||||
(
|
||||
'Don\'t get too cocky with that energy shield; you can still get '
|
||||
'yourself thrown off a cliff.'
|
||||
),
|
||||
(
|
||||
'Many things can be picked up and thrown,'
|
||||
' including other players. '
|
||||
'Tossing\nyour enemies off cliffs can be an effective and '
|
||||
'emotionally fulfilling strategy.'
|
||||
),
|
||||
(
|
||||
'Ice bombs are not very powerful, but they freeze\nwhoever they '
|
||||
'hit, leaving them vulnerable to shattering.'
|
||||
),
|
||||
'Don\'t spin for too long; you\'ll become dizzy and fall.',
|
||||
('Run back and forth before throwing a bomb\nto \'whiplash\' it '
|
||||
'and throw it farther.'),
|
||||
('Punches do more damage the faster your fists are moving,\nso '
|
||||
'try running, jumping, and spinning like crazy.'),
|
||||
(
|
||||
'Run back and forth before throwing a bomb\nto \'whiplash\' it '
|
||||
'and throw it farther.'
|
||||
),
|
||||
(
|
||||
'Punches do more damage the faster your fists are moving,\nso '
|
||||
'try running, jumping, and spinning like crazy.'
|
||||
),
|
||||
'In hockey, you\'ll maintain more speed if you turn gradually.',
|
||||
('The head is the most vulnerable area, so a sticky-bomb\nto the '
|
||||
'noggin usually means game-over.'),
|
||||
('Hold down any button to run. You\'ll get places faster\nbut '
|
||||
'won\'t turn very well, so watch out for cliffs.'),
|
||||
('You can judge when a bomb is going to explode based on the\n'
|
||||
'color of sparks from its fuse: yellow..orange..red..BOOM.'),
|
||||
(
|
||||
'The head is the most vulnerable area, so a sticky-bomb\nto the '
|
||||
'noggin usually means game-over.'
|
||||
),
|
||||
(
|
||||
'Hold down any button to run. You\'ll get places faster\nbut '
|
||||
'won\'t turn very well, so watch out for cliffs.'
|
||||
),
|
||||
(
|
||||
'You can judge when a bomb is going to explode based on the\n'
|
||||
'color of sparks from its fuse: yellow..orange..red..BOOM.'
|
||||
),
|
||||
]
|
||||
app = _ba.app
|
||||
if not app.iircade_mode:
|
||||
|
|
@ -81,12 +122,17 @@ def get_all_tips() -> list[str]:
|
|||
'If your framerate is choppy, try turning down resolution\nor '
|
||||
'visuals in the game\'s graphics settings.'
|
||||
]
|
||||
if (app.platform in ('android', 'ios') and not app.on_tv
|
||||
and not app.iircade_mode):
|
||||
if (
|
||||
app.platform in ('android', 'ios')
|
||||
and not app.on_tv
|
||||
and not app.iircade_mode
|
||||
):
|
||||
tips += [
|
||||
('If your device gets too warm or you\'d like to conserve '
|
||||
'battery power,\nturn down "Visuals" or "Resolution" '
|
||||
'in Settings->Graphics'),
|
||||
(
|
||||
'If your device gets too warm or you\'d like to conserve '
|
||||
'battery power,\nturn down "Visuals" or "Resolution" '
|
||||
'in Settings->Graphics'
|
||||
),
|
||||
]
|
||||
if app.platform in ['mac', 'android'] and not app.iircade_mode:
|
||||
tips += [
|
||||
|
|
|
|||
22
dist/ba_data/python/ba/_tournament.py
vendored
22
dist/ba_data/python/ba/_tournament.py
vendored
|
|
@ -17,6 +17,7 @@ def get_tournament_prize_strings(entry: dict[str, Any]) -> list[str]:
|
|||
# pylint: disable=too-many-locals
|
||||
from ba._generated.enums import SpecialChar
|
||||
from ba._gameutils import get_trophy_string
|
||||
|
||||
range1 = entry.get('prizeRange1')
|
||||
range2 = entry.get('prizeRange2')
|
||||
range3 = entry.get('prizeRange3')
|
||||
|
|
@ -27,12 +28,18 @@ def get_tournament_prize_strings(entry: dict[str, Any]) -> list[str]:
|
|||
trophy_type_2 = entry.get('prizeTrophy2')
|
||||
trophy_type_3 = entry.get('prizeTrophy3')
|
||||
out_vals = []
|
||||
for rng, prize, trophy_type in ((range1, prize1, trophy_type_1),
|
||||
(range2, prize2, trophy_type_2),
|
||||
(range3, prize3, trophy_type_3)):
|
||||
prval = ('' if rng is None else ('#' + str(rng[0])) if
|
||||
(rng[0] == rng[1]) else
|
||||
('#' + str(rng[0]) + '-' + str(rng[1])))
|
||||
for rng, prize, trophy_type in (
|
||||
(range1, prize1, trophy_type_1),
|
||||
(range2, prize2, trophy_type_2),
|
||||
(range3, prize3, trophy_type_3),
|
||||
):
|
||||
prval = (
|
||||
''
|
||||
if rng is None
|
||||
else ('#' + str(rng[0]))
|
||||
if (rng[0] == rng[1])
|
||||
else ('#' + str(rng[0]) + '-' + str(rng[1]))
|
||||
)
|
||||
pvval = ''
|
||||
if trophy_type is not None:
|
||||
pvval += get_trophy_string(trophy_type)
|
||||
|
|
@ -40,8 +47,7 @@ def get_tournament_prize_strings(entry: dict[str, Any]) -> list[str]:
|
|||
# If we've got trophies but not for this entry, throw some space
|
||||
# in to compensate so the ticket counts line up.
|
||||
if prize is not None:
|
||||
pvval = _ba.charstr(
|
||||
SpecialChar.TICKET_BACKING) + str(prize) + pvval
|
||||
pvval = _ba.charstr(SpecialChar.TICKET_BACKING) + str(prize) + pvval
|
||||
out_vals.append(prval)
|
||||
out_vals.append(pvval)
|
||||
return out_vals
|
||||
|
|
|
|||
31
dist/ba_data/python/ba/_ui.py
vendored
31
dist/ba_data/python/ba/_ui.py
vendored
|
|
@ -89,19 +89,21 @@ class UISubsystem:
|
|||
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))
|
||||
_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)
|
||||
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."""
|
||||
|
|
@ -122,6 +124,7 @@ class UISubsystem:
|
|||
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
|
||||
|
|
@ -136,9 +139,12 @@ class UISubsystem:
|
|||
# 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}')
|
||||
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)
|
||||
|
|
@ -148,8 +154,9 @@ class UISubsystem:
|
|||
"""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)
|
||||
_ba.containerwidget(
|
||||
edit=self._main_menu_window, transition=transition
|
||||
)
|
||||
else:
|
||||
self._main_menu_window.delete()
|
||||
|
||||
|
|
|
|||
64
dist/ba_data/python/ba/_workspace.py
vendored
64
dist/ba_data/python/ba/_workspace.py
vendored
|
|
@ -36,6 +36,7 @@ class WorkspaceSubsystem:
|
|||
|
||||
def set_active_workspace(
|
||||
self,
|
||||
account: ba.AccountV2Handle,
|
||||
workspaceid: str,
|
||||
workspacename: str,
|
||||
on_completed: Callable[[], None],
|
||||
|
|
@ -46,9 +47,11 @@ class WorkspaceSubsystem:
|
|||
# interactivity.
|
||||
Thread(
|
||||
target=lambda: self._set_active_workspace_bg(
|
||||
account=account,
|
||||
workspaceid=workspaceid,
|
||||
workspacename=workspacename,
|
||||
on_completed=on_completed),
|
||||
on_completed=on_completed,
|
||||
),
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
|
|
@ -60,16 +63,22 @@ class WorkspaceSubsystem:
|
|||
_ba.screenmessage(msg, color=(0, 1, 0))
|
||||
_ba.playsound(_ba.getsound('gunCocking'))
|
||||
|
||||
def _set_active_workspace_bg(self, workspaceid: str, workspacename: str,
|
||||
on_completed: Callable[[], None]) -> None:
|
||||
def _set_active_workspace_bg(
|
||||
self,
|
||||
account: ba.AccountV2Handle,
|
||||
workspaceid: str,
|
||||
workspacename: str,
|
||||
on_completed: Callable[[], None],
|
||||
) -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
class _SkipSyncError(RuntimeError):
|
||||
pass
|
||||
|
||||
set_path = True
|
||||
wspath = Path(_ba.get_volatile_data_directory(), 'workspaces',
|
||||
workspaceid)
|
||||
wspath = Path(
|
||||
_ba.get_volatile_data_directory(), 'workspaces', workspaceid
|
||||
)
|
||||
try:
|
||||
|
||||
# If it seems we're offline, don't even attempt a sync,
|
||||
|
|
@ -85,15 +94,20 @@ class WorkspaceSubsystem:
|
|||
state = bacommon.cloud.WorkspaceFetchState(manifest=manifest)
|
||||
|
||||
while True:
|
||||
response = _ba.app.cloud.send_message(
|
||||
bacommon.cloud.WorkspaceFetchMessage(
|
||||
workspaceid=workspaceid, state=state))
|
||||
with account:
|
||||
response = _ba.app.cloud.send_message(
|
||||
bacommon.cloud.WorkspaceFetchMessage(
|
||||
workspaceid=workspaceid, state=state
|
||||
)
|
||||
)
|
||||
state = response.state
|
||||
self._handle_deletes(workspace_dir=wspath,
|
||||
deletes=response.deletes)
|
||||
self._handle_deletes(
|
||||
workspace_dir=wspath, deletes=response.deletes
|
||||
)
|
||||
self._handle_downloads_inline(
|
||||
workspace_dir=wspath,
|
||||
downloads_inline=response.downloads_inline)
|
||||
downloads_inline=response.downloads_inline,
|
||||
)
|
||||
if response.done:
|
||||
# Server only deals in files; let's clean up any
|
||||
# leftover empty dirs after the dust has cleared.
|
||||
|
|
@ -104,8 +118,10 @@ class WorkspaceSubsystem:
|
|||
_ba.pushcall(
|
||||
tpartial(
|
||||
self._successmsg,
|
||||
Lstr(resource='activatedText',
|
||||
subs=[('${THING}', workspacename)]),
|
||||
Lstr(
|
||||
resource='activatedText',
|
||||
subs=[('${THING}', workspacename)],
|
||||
),
|
||||
),
|
||||
from_other_thread=True,
|
||||
)
|
||||
|
|
@ -114,8 +130,11 @@ class WorkspaceSubsystem:
|
|||
_ba.pushcall(
|
||||
tpartial(
|
||||
self._errmsg,
|
||||
Lstr(resource='workspaceSyncReuseText',
|
||||
subs=[('${WORKSPACE}', workspacename)])),
|
||||
Lstr(
|
||||
resource='workspaceSyncReuseText',
|
||||
subs=[('${WORKSPACE}', workspacename)],
|
||||
),
|
||||
),
|
||||
from_other_thread=True,
|
||||
)
|
||||
|
||||
|
|
@ -123,8 +142,10 @@ class WorkspaceSubsystem:
|
|||
# Avoid reusing existing if we fail in the middle; could
|
||||
# be in wonky state.
|
||||
set_path = False
|
||||
_ba.pushcall(tpartial(self._errmsg, Lstr(value=str(exc))),
|
||||
from_other_thread=True)
|
||||
_ba.pushcall(
|
||||
tpartial(self._errmsg, Lstr(value=str(exc))),
|
||||
from_other_thread=True,
|
||||
)
|
||||
except Exception:
|
||||
# Ditto.
|
||||
set_path = False
|
||||
|
|
@ -132,8 +153,10 @@ class WorkspaceSubsystem:
|
|||
_ba.pushcall(
|
||||
tpartial(
|
||||
self._errmsg,
|
||||
Lstr(resource='workspaceSyncErrorText',
|
||||
subs=[('${WORKSPACE}', workspacename)]),
|
||||
Lstr(
|
||||
resource='workspaceSyncErrorText',
|
||||
subs=[('${WORKSPACE}', workspacename)],
|
||||
),
|
||||
),
|
||||
from_other_thread=True,
|
||||
)
|
||||
|
|
@ -186,8 +209,7 @@ class WorkspaceSubsystem:
|
|||
# listed when the parent dir is visited, so lets make sure
|
||||
# to only acknowledge still-existing ones.
|
||||
dirnames = [
|
||||
d for d in dirnames
|
||||
if os.path.exists(os.path.join(basename, d))
|
||||
d for d in dirnames if os.path.exists(os.path.join(basename, d))
|
||||
]
|
||||
if not dirnames and not filenames and basename != prunedir:
|
||||
os.rmdir(basename)
|
||||
|
|
|
|||
206
dist/ba_data/python/ba/internal.py
vendored
206
dist/ba_data/python/ba/internal.py
vendored
|
|
@ -9,73 +9,173 @@ defensively) in mods.
|
|||
from __future__ import annotations
|
||||
|
||||
from _ba import (
|
||||
show_online_score_ui, set_ui_input_device, is_party_icon_visible,
|
||||
getinputdevice, add_clean_frame_callback, unlock_all_input,
|
||||
increment_analytics_count, set_debug_speed_exponent, get_special_widget,
|
||||
get_qrcode_texture, get_string_height, get_string_width, show_app_invite,
|
||||
appnameupper, lock_all_input, open_file_externally, fade_screen, appname,
|
||||
have_incentivized_ad, has_video_ads, workspaces_in_use,
|
||||
set_party_icon_always_visible, connect_to_party, get_game_port,
|
||||
end_host_scanning, host_scan_cycle, charstr, get_public_party_enabled,
|
||||
get_public_party_max_size, set_public_party_name,
|
||||
set_public_party_max_size, set_authenticate_clients,
|
||||
set_public_party_enabled, reset_random_player_names, new_host_session,
|
||||
get_foreground_host_session, get_local_active_input_devices_count,
|
||||
get_ui_input_device, is_in_replay, set_replay_speed_exponent,
|
||||
get_replay_speed_exponent, disconnect_from_host, set_party_window_open,
|
||||
get_connection_to_host_info, get_chat_messages, get_game_roster,
|
||||
disconnect_client, chatmessage, get_random_names, have_permission,
|
||||
request_permission, have_touchscreen_input, is_xcode_build,
|
||||
set_low_level_config_value, get_low_level_config_value,
|
||||
capture_gamepad_input, release_gamepad_input, has_gamma_control,
|
||||
get_max_graphics_quality, get_display_resolution, capture_keyboard_input,
|
||||
release_keyboard_input, value_test, set_touchscreen_editing,
|
||||
is_running_on_fire_tv, android_get_external_files_dir,
|
||||
set_telnet_access_enabled, new_replay_session, get_replays_dir)
|
||||
show_online_score_ui,
|
||||
set_ui_input_device,
|
||||
is_party_icon_visible,
|
||||
getinputdevice,
|
||||
add_clean_frame_callback,
|
||||
unlock_all_input,
|
||||
increment_analytics_count,
|
||||
set_debug_speed_exponent,
|
||||
get_special_widget,
|
||||
get_qrcode_texture,
|
||||
get_string_height,
|
||||
get_string_width,
|
||||
show_app_invite,
|
||||
appnameupper,
|
||||
lock_all_input,
|
||||
open_file_externally,
|
||||
fade_screen,
|
||||
appname,
|
||||
have_incentivized_ad,
|
||||
has_video_ads,
|
||||
workspaces_in_use,
|
||||
set_party_icon_always_visible,
|
||||
connect_to_party,
|
||||
get_game_port,
|
||||
end_host_scanning,
|
||||
host_scan_cycle,
|
||||
charstr,
|
||||
get_public_party_enabled,
|
||||
get_public_party_max_size,
|
||||
set_public_party_name,
|
||||
set_public_party_max_size,
|
||||
set_authenticate_clients,
|
||||
set_public_party_enabled,
|
||||
reset_random_player_names,
|
||||
new_host_session,
|
||||
get_foreground_host_session,
|
||||
get_local_active_input_devices_count,
|
||||
get_ui_input_device,
|
||||
is_in_replay,
|
||||
set_replay_speed_exponent,
|
||||
get_replay_speed_exponent,
|
||||
disconnect_from_host,
|
||||
set_party_window_open,
|
||||
get_connection_to_host_info,
|
||||
get_chat_messages,
|
||||
get_game_roster,
|
||||
disconnect_client,
|
||||
chatmessage,
|
||||
get_random_names,
|
||||
have_permission,
|
||||
request_permission,
|
||||
have_touchscreen_input,
|
||||
is_xcode_build,
|
||||
set_low_level_config_value,
|
||||
get_low_level_config_value,
|
||||
capture_gamepad_input,
|
||||
release_gamepad_input,
|
||||
has_gamma_control,
|
||||
get_max_graphics_quality,
|
||||
get_display_resolution,
|
||||
capture_keyboard_input,
|
||||
release_keyboard_input,
|
||||
value_test,
|
||||
set_touchscreen_editing,
|
||||
is_running_on_fire_tv,
|
||||
android_get_external_files_dir,
|
||||
set_telnet_access_enabled,
|
||||
new_replay_session,
|
||||
get_replays_dir,
|
||||
)
|
||||
|
||||
from ba._map import (get_map_class, register_map, preload_map_preview_media,
|
||||
get_map_display_string, get_filtered_map_name)
|
||||
from ba._map import (
|
||||
get_map_class,
|
||||
register_map,
|
||||
preload_map_preview_media,
|
||||
get_map_display_string,
|
||||
get_filtered_map_name,
|
||||
)
|
||||
from ba._appconfig import commit_app_config
|
||||
from ba._input import (get_device_value, get_input_map_hash,
|
||||
get_input_device_config)
|
||||
from ba._input import (
|
||||
get_device_value,
|
||||
get_input_map_hash,
|
||||
get_input_device_config,
|
||||
)
|
||||
from ba._general import getclass, json_prep, get_type_name
|
||||
from ba._activitytypes import JoinActivity, ScoreScreenActivity
|
||||
from ba._apputils import (is_browser_likely_available, get_remote_app_name,
|
||||
should_submit_debug_info)
|
||||
from ba._benchmark import (run_gpu_benchmark, run_cpu_benchmark,
|
||||
run_media_reload_benchmark, run_stress_test)
|
||||
from ba._apputils import (
|
||||
is_browser_likely_available,
|
||||
get_remote_app_name,
|
||||
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 getcampaign
|
||||
from ba._messages import PlayerProfilesChangedMessage
|
||||
from ba._multiteamsession import DEFAULT_TEAM_COLORS, DEFAULT_TEAM_NAMES
|
||||
from ba._music import do_play_music
|
||||
from ba._net import (master_server_get, master_server_post,
|
||||
get_ip_address_type, DEFAULT_REQUEST_TIMEOUT_SECONDS)
|
||||
from ba._net import (
|
||||
master_server_get,
|
||||
master_server_post,
|
||||
get_ip_address_type,
|
||||
DEFAULT_REQUEST_TIMEOUT_SECONDS,
|
||||
)
|
||||
from ba._powerup import get_default_powerup_distribution
|
||||
from ba._profile import (get_player_profile_colors, get_player_profile_icon,
|
||||
get_player_colors)
|
||||
from ba._profile import (
|
||||
get_player_profile_colors,
|
||||
get_player_profile_icon,
|
||||
get_player_colors,
|
||||
)
|
||||
from ba._tips import get_next_tip
|
||||
from ba._playlist import (get_default_free_for_all_playlist,
|
||||
get_default_teams_playlist, filter_playlist)
|
||||
from ba._store import (get_available_sale_time, get_available_purchase_count,
|
||||
get_store_item_name_translated,
|
||||
get_store_item_display_size, get_store_layout,
|
||||
get_store_item, get_clean_price, get_unowned_maps,
|
||||
get_unowned_game_types)
|
||||
from ba._playlist import (
|
||||
get_default_free_for_all_playlist,
|
||||
get_default_teams_playlist,
|
||||
filter_playlist,
|
||||
)
|
||||
from ba._store import (
|
||||
get_available_sale_time,
|
||||
get_available_purchase_count,
|
||||
get_store_item_name_translated,
|
||||
get_store_item_display_size,
|
||||
get_store_layout,
|
||||
get_store_item,
|
||||
get_clean_price,
|
||||
get_unowned_maps,
|
||||
get_unowned_game_types,
|
||||
)
|
||||
from ba._tournament import get_tournament_prize_strings
|
||||
from ba._gameutils import get_trophy_string
|
||||
|
||||
from ba._internal import (
|
||||
get_v2_fleet, get_master_server_address, is_blessed, get_news_show,
|
||||
game_service_has_leaderboard, report_achievement, submit_score,
|
||||
tournament_query, power_ranking_query, restore_purchases, purchase,
|
||||
get_purchases_state, get_purchased, get_price, in_game_purchase,
|
||||
add_transaction, reset_achievements, get_public_login_id,
|
||||
have_outstanding_transactions, run_transactions,
|
||||
get_v1_account_misc_read_val, get_v1_account_misc_read_val_2,
|
||||
get_v1_account_misc_val, get_v1_account_ticket_count,
|
||||
get_v1_account_state_num, get_v1_account_state,
|
||||
get_v1_account_display_string, get_v1_account_type, get_v1_account_name,
|
||||
sign_out_v1, sign_in_v1, mark_config_dirty)
|
||||
get_v2_fleet,
|
||||
get_master_server_address,
|
||||
is_blessed,
|
||||
get_news_show,
|
||||
game_service_has_leaderboard,
|
||||
report_achievement,
|
||||
submit_score,
|
||||
tournament_query,
|
||||
power_ranking_query,
|
||||
restore_purchases,
|
||||
purchase,
|
||||
get_purchases_state,
|
||||
get_purchased,
|
||||
get_price,
|
||||
in_game_purchase,
|
||||
add_transaction,
|
||||
reset_achievements,
|
||||
get_public_login_id,
|
||||
have_outstanding_transactions,
|
||||
run_transactions,
|
||||
get_v1_account_misc_read_val,
|
||||
get_v1_account_misc_read_val_2,
|
||||
get_v1_account_misc_val,
|
||||
get_v1_account_ticket_count,
|
||||
get_v1_account_state_num,
|
||||
get_v1_account_state,
|
||||
get_v1_account_display_string,
|
||||
get_v1_account_type,
|
||||
get_v1_account_name,
|
||||
sign_out_v1,
|
||||
sign_in_v1,
|
||||
mark_config_dirty,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'show_online_score_ui',
|
||||
|
|
|
|||
84
dist/ba_data/python/ba/macmusicapp.py
vendored
84
dist/ba_data/python/ba/macmusicapp.py
vendored
|
|
@ -24,12 +24,18 @@ class MacMusicAppMusicPlayer(MusicPlayer):
|
|||
self._thread = _MacMusicAppThread()
|
||||
self._thread.start()
|
||||
|
||||
def on_select_entry(self, callback: Callable[[Any], None],
|
||||
current_entry: Any, selection_target_name: str) -> Any:
|
||||
def on_select_entry(
|
||||
self,
|
||||
callback: Callable[[Any], None],
|
||||
current_entry: Any,
|
||||
selection_target_name: str,
|
||||
) -> Any:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.soundtrack import entrytypeselect as etsel
|
||||
return etsel.SoundtrackEntryTypeSelectWindow(callback, current_entry,
|
||||
selection_target_name)
|
||||
|
||||
return etsel.SoundtrackEntryTypeSelectWindow(
|
||||
callback, current_entry, selection_target_name
|
||||
)
|
||||
|
||||
def on_set_volume(self, volume: float) -> None:
|
||||
self._thread.set_volume(volume)
|
||||
|
|
@ -44,8 +50,10 @@ class MacMusicAppMusicPlayer(MusicPlayer):
|
|||
if entry_type == 'iTunesPlaylist':
|
||||
self._thread.play_playlist(music.get_soundtrack_entry_name(entry))
|
||||
else:
|
||||
print('MacMusicAppMusicPlayer passed unrecognized entry type:',
|
||||
entry_type)
|
||||
print(
|
||||
'MacMusicAppMusicPlayer passed unrecognized entry type:',
|
||||
entry_type,
|
||||
)
|
||||
|
||||
def on_stop(self) -> None:
|
||||
self._thread.play_playlist(None)
|
||||
|
|
@ -70,6 +78,7 @@ class _MacMusicAppThread(threading.Thread):
|
|||
from ba._general import Call
|
||||
from ba._language import Lstr
|
||||
from ba._generated.enums import TimeType
|
||||
|
||||
_ba.set_thread_name('BA_MacMusicAppThread')
|
||||
_ba.mac_music_app_init()
|
||||
|
||||
|
|
@ -77,10 +86,15 @@ class _MacMusicAppThread(threading.Thread):
|
|||
# it causes any funny business (this used to background the app
|
||||
# sometimes, though I think that is fixed now)
|
||||
def do_print() -> None:
|
||||
_ba.timer(1.0,
|
||||
Call(_ba.screenmessage, Lstr(resource='usingItunesText'),
|
||||
(0, 1, 0)),
|
||||
timetype=TimeType.REAL)
|
||||
_ba.timer(
|
||||
1.0,
|
||||
Call(
|
||||
_ba.screenmessage,
|
||||
Lstr(resource='usingItunesText'),
|
||||
(0, 1, 0),
|
||||
),
|
||||
timetype=TimeType.REAL,
|
||||
)
|
||||
|
||||
_ba.pushcall(do_print, from_other_thread=True)
|
||||
|
||||
|
|
@ -153,15 +167,29 @@ class _MacMusicAppThread(threading.Thread):
|
|||
self._commands_available.set()
|
||||
|
||||
def _handle_get_playlists_command(
|
||||
self, target: Callable[[list[str]], None]) -> None:
|
||||
self, target: Callable[[list[str]], None]
|
||||
) -> None:
|
||||
from ba._general import Call
|
||||
|
||||
try:
|
||||
playlists = _ba.mac_music_app_get_playlists()
|
||||
playlists = [
|
||||
p for p in playlists if p not in [
|
||||
'Music', 'Movies', 'TV Shows', 'Podcasts', 'iTunes\xa0U',
|
||||
'Books', 'Genius', 'iTunes DJ', 'Music Videos',
|
||||
'Home Videos', 'Voice Memos', 'Audiobooks'
|
||||
p
|
||||
for p in playlists
|
||||
if p
|
||||
not in [
|
||||
'Music',
|
||||
'Movies',
|
||||
'TV Shows',
|
||||
'Podcasts',
|
||||
'iTunes\xa0U',
|
||||
'Books',
|
||||
'Genius',
|
||||
'iTunes DJ',
|
||||
'Music Videos',
|
||||
'Home Videos',
|
||||
'Voice Memos',
|
||||
'Audiobooks',
|
||||
]
|
||||
]
|
||||
playlists.sort(key=lambda x: x.lower())
|
||||
|
|
@ -194,7 +222,7 @@ class _MacMusicAppThread(threading.Thread):
|
|||
# Set our playlist and play it if our volume is up.
|
||||
self._current_playlist = target
|
||||
if self._volume > 0:
|
||||
self._orig_volume = (_ba.mac_music_app_get_volume())
|
||||
self._orig_volume = _ba.mac_music_app_get_volume()
|
||||
self._update_mac_music_app_volume()
|
||||
self._play_current_playlist()
|
||||
|
||||
|
|
@ -213,20 +241,30 @@ class _MacMusicAppThread(threading.Thread):
|
|||
def _play_current_playlist(self) -> None:
|
||||
try:
|
||||
from ba._general import Call
|
||||
|
||||
assert self._current_playlist is not None
|
||||
if _ba.mac_music_app_play_playlist(self._current_playlist):
|
||||
pass
|
||||
else:
|
||||
_ba.pushcall(Call(
|
||||
_ba.screenmessage,
|
||||
_ba.app.lang.get_resource('playlistNotFoundText') +
|
||||
': \'' + self._current_playlist + '\'', (1, 0, 0)),
|
||||
from_other_thread=True)
|
||||
_ba.pushcall(
|
||||
Call(
|
||||
_ba.screenmessage,
|
||||
_ba.app.lang.get_resource('playlistNotFoundText')
|
||||
+ ': \''
|
||||
+ self._current_playlist
|
||||
+ '\'',
|
||||
(1, 0, 0),
|
||||
),
|
||||
from_other_thread=True,
|
||||
)
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception(
|
||||
f'error playing playlist {self._current_playlist}')
|
||||
f'error playing playlist {self._current_playlist}'
|
||||
)
|
||||
|
||||
def _update_mac_music_app_volume(self) -> None:
|
||||
_ba.mac_music_app_set_volume(
|
||||
max(0, min(100, int(100.0 * self._volume))))
|
||||
max(0, min(100, int(100.0 * self._volume)))
|
||||
)
|
||||
|
|
|
|||
45
dist/ba_data/python/ba/modutils.py
vendored
45
dist/ba_data/python/ba/modutils.py
vendored
|
|
@ -47,10 +47,12 @@ def _request_storage_permission() -> bool:
|
|||
"""If needed, requests storage permission from the user (& return true)."""
|
||||
from ba._language import Lstr
|
||||
from ba._generated.enums import Permission
|
||||
|
||||
if not _ba.have_permission(Permission.STORAGE):
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
_ba.screenmessage(Lstr(resource='storagePermissionAccessText'),
|
||||
color=(1, 0, 0))
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='storagePermissionAccessText'), color=(1, 0, 0)
|
||||
)
|
||||
_ba.timer(1.0, lambda: _ba.request_permission(Permission.STORAGE))
|
||||
return True
|
||||
return False
|
||||
|
|
@ -80,12 +82,15 @@ def show_user_scripts() -> None:
|
|||
if usd is not None and os.path.isdir(usd):
|
||||
file_name = usd + '/about_this_folder.txt'
|
||||
with open(file_name, 'w', encoding='utf-8') as outfile:
|
||||
outfile.write('You can drop files in here to mod the game.'
|
||||
' See settings/advanced'
|
||||
' in the game for more info.')
|
||||
outfile.write(
|
||||
'You can drop files in here to mod the game.'
|
||||
' See settings/advanced'
|
||||
' in the game for more info.'
|
||||
)
|
||||
_ba.android_media_scan_file(file_name)
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception('error writing about_this_folder stuff')
|
||||
|
||||
# On a few platforms we try to open the dir in the UI.
|
||||
|
|
@ -103,13 +108,14 @@ def create_user_system_scripts() -> None:
|
|||
(for editing and experiment with)
|
||||
"""
|
||||
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)
|
||||
path = app.python_directory_user + '/sys/' + app.version
|
||||
pathtmp = path + '_tmp'
|
||||
if os.path.exists(path):
|
||||
shutil.rmtree(path)
|
||||
|
|
@ -123,31 +129,38 @@ def create_user_system_scripts() -> None:
|
|||
# to blow them away anyway to make changes;
|
||||
# See https://github.com/efroemling/ballistica/wiki
|
||||
# /Knowledge-Nuggets#python-cache-files-gotcha
|
||||
return ('__pycache__', )
|
||||
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)')
|
||||
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.')
|
||||
print(
|
||||
'Note: the new files may not be visible via '
|
||||
'android-file-transfer until you restart your device.'
|
||||
)
|
||||
|
||||
|
||||
def delete_user_system_scripts() -> None:
|
||||
"""Clean out the scripts created by create_user_system_scripts()."""
|
||||
import shutil
|
||||
|
||||
app = _ba.app
|
||||
path = (app.python_directory_user + '/sys/' + app.version)
|
||||
path = app.python_directory_user + '/sys/' + app.version
|
||||
if os.path.exists(path):
|
||||
shutil.rmtree(path)
|
||||
print(f'User system scripts deleted.\n'
|
||||
f'Restart {_ba.appname()} to use internal'
|
||||
f' scripts. (use ba.quit() to exit the game)')
|
||||
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.')
|
||||
|
||||
|
|
|
|||
87
dist/ba_data/python/ba/osmusic.py
vendored
87
dist/ba_data/python/ba/osmusic.py
vendored
|
|
@ -31,13 +31,20 @@ class OSMusicPlayer(MusicPlayer):
|
|||
# FIXME: should ask the C++ layer for these; just hard-coding for now.
|
||||
return ['mp3', 'ogg', 'm4a', 'wav', 'flac', 'mid']
|
||||
|
||||
def on_select_entry(self, callback: Callable[[Any], None],
|
||||
current_entry: Any, selection_target_name: str) -> Any:
|
||||
def on_select_entry(
|
||||
self,
|
||||
callback: Callable[[Any], None],
|
||||
current_entry: Any,
|
||||
selection_target_name: str,
|
||||
) -> Any:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.soundtrack.entrytypeselect import (
|
||||
SoundtrackEntryTypeSelectWindow)
|
||||
return SoundtrackEntryTypeSelectWindow(callback, current_entry,
|
||||
selection_target_name)
|
||||
SoundtrackEntryTypeSelectWindow,
|
||||
)
|
||||
|
||||
return SoundtrackEntryTypeSelectWindow(
|
||||
callback, current_entry, selection_target_name
|
||||
)
|
||||
|
||||
def on_set_volume(self, volume: float) -> None:
|
||||
_ba.music_player_set_volume(volume)
|
||||
|
|
@ -56,22 +63,31 @@ class OSMusicPlayer(MusicPlayer):
|
|||
# valid file within it.
|
||||
self._want_to_play = True
|
||||
self._actually_playing = False
|
||||
_PickFolderSongThread(name, self.get_valid_music_file_extensions(),
|
||||
self._on_play_folder_cb).start()
|
||||
_PickFolderSongThread(
|
||||
name,
|
||||
self.get_valid_music_file_extensions(),
|
||||
self._on_play_folder_cb,
|
||||
).start()
|
||||
|
||||
def _on_play_folder_cb(self,
|
||||
result: str | list[str],
|
||||
error: str | None = None) -> None:
|
||||
def _on_play_folder_cb(
|
||||
self, result: str | list[str], error: str | None = None
|
||||
) -> None:
|
||||
from ba import _language
|
||||
|
||||
if error is not None:
|
||||
rstr = (_language.Lstr(
|
||||
resource='internal.errorPlayingMusicText').evaluate())
|
||||
rstr = _language.Lstr(
|
||||
resource='internal.errorPlayingMusicText'
|
||||
).evaluate()
|
||||
if isinstance(result, str):
|
||||
err_str = (rstr.replace('${MUSIC}', os.path.basename(result)) +
|
||||
'; ' + str(error))
|
||||
err_str = (
|
||||
rstr.replace('${MUSIC}', os.path.basename(result))
|
||||
+ '; '
|
||||
+ str(error)
|
||||
)
|
||||
else:
|
||||
err_str = (rstr.replace('${MUSIC}', '<multiple>') + '; ' +
|
||||
str(error))
|
||||
err_str = (
|
||||
rstr.replace('${MUSIC}', '<multiple>') + '; ' + str(error)
|
||||
)
|
||||
_ba.screenmessage(err_str, color=(1, 0, 0))
|
||||
return
|
||||
|
||||
|
|
@ -93,9 +109,12 @@ class OSMusicPlayer(MusicPlayer):
|
|||
|
||||
|
||||
class _PickFolderSongThread(threading.Thread):
|
||||
|
||||
def __init__(self, path: str, valid_extensions: list[str],
|
||||
callback: Callable[[str | list[str], str | None], None]):
|
||||
def __init__(
|
||||
self,
|
||||
path: str,
|
||||
valid_extensions: list[str],
|
||||
callback: Callable[[str | list[str], str | None], None],
|
||||
):
|
||||
super().__init__()
|
||||
self._valid_extensions = valid_extensions
|
||||
self._callback = callback
|
||||
|
|
@ -104,6 +123,7 @@ class _PickFolderSongThread(threading.Thread):
|
|||
def run(self) -> None:
|
||||
from ba import _language
|
||||
from ba._general import Call
|
||||
|
||||
do_print_error = True
|
||||
try:
|
||||
_ba.set_thread_name('BA_PickFolderSongThread')
|
||||
|
|
@ -111,24 +131,33 @@ class _PickFolderSongThread(threading.Thread):
|
|||
valid_extensions = ['.' + x for x in self._valid_extensions]
|
||||
for root, _subdirs, filenames in os.walk(self._path):
|
||||
for fname in filenames:
|
||||
if any(fname.lower().endswith(ext)
|
||||
for ext in valid_extensions):
|
||||
all_files.insert(random.randrange(len(all_files) + 1),
|
||||
root + '/' + fname)
|
||||
if any(
|
||||
fname.lower().endswith(ext) for ext in valid_extensions
|
||||
):
|
||||
all_files.insert(
|
||||
random.randrange(len(all_files) + 1),
|
||||
root + '/' + fname,
|
||||
)
|
||||
if not all_files:
|
||||
do_print_error = False
|
||||
raise RuntimeError(
|
||||
_language.Lstr(resource='internal.noMusicFilesInFolderText'
|
||||
).evaluate())
|
||||
_ba.pushcall(Call(self._callback, all_files, None),
|
||||
from_other_thread=True)
|
||||
_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
|
||||
|
||||
if do_print_error:
|
||||
_error.print_exception()
|
||||
try:
|
||||
err_str = str(exc)
|
||||
except Exception:
|
||||
err_str = '<ENCERR4523>'
|
||||
_ba.pushcall(Call(self._callback, self._path, err_str),
|
||||
from_other_thread=True)
|
||||
_ba.pushcall(
|
||||
Call(self._callback, self._path, err_str),
|
||||
from_other_thread=True,
|
||||
)
|
||||
|
|
|
|||
33
dist/ba_data/python/ba/ui/__init__.py
vendored
33
dist/ba_data/python/ba/ui/__init__.py
vendored
|
|
@ -11,7 +11,6 @@ from typing import TYPE_CHECKING, cast, Type
|
|||
|
||||
import _ba
|
||||
from ba._generated.enums import TimeType
|
||||
from ba._general import print_active_refs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
|
@ -44,6 +43,7 @@ class Window:
|
|||
@dataclass
|
||||
class UICleanupCheck:
|
||||
"""Holds info about a uicleanupcheck target."""
|
||||
|
||||
obj: weakref.ref
|
||||
widget: ba.Widget
|
||||
widget_death_time: float | None
|
||||
|
|
@ -112,6 +112,7 @@ class UIEntry:
|
|||
# TEMP HARD CODED - WILL REPLACE THIS WITH BA_META LOOKUPS.
|
||||
if self._name == 'mainmenu':
|
||||
from bastd.ui import mainmenu
|
||||
|
||||
return cast(Type[UILocation], mainmenu.MainMenuWindow)
|
||||
raise ValueError('unknown ui class ' + str(self._name))
|
||||
|
||||
|
|
@ -139,8 +140,9 @@ class UIController:
|
|||
"""Show the main menu, clearing other UIs from location stacks."""
|
||||
self._main_stack = []
|
||||
self._dialog_stack = []
|
||||
self._main_stack = (self._main_stack_game
|
||||
if in_game else self._main_stack_menu)
|
||||
self._main_stack = (
|
||||
self._main_stack_game if in_game else self._main_stack_menu
|
||||
)
|
||||
self._main_stack.append(UIEntry('mainmenu', self))
|
||||
self._update_ui()
|
||||
|
||||
|
|
@ -154,8 +156,13 @@ class UIController:
|
|||
entry.destroy()
|
||||
|
||||
# Now create the topmost one if there is one.
|
||||
entrynew = (self._dialog_stack[-1] if self._dialog_stack else
|
||||
self._main_stack[-1] if self._main_stack else None)
|
||||
entrynew = (
|
||||
self._dialog_stack[-1]
|
||||
if self._dialog_stack
|
||||
else self._main_stack[-1]
|
||||
if self._main_stack
|
||||
else None
|
||||
)
|
||||
if entrynew is not None:
|
||||
entrynew.create()
|
||||
|
||||
|
|
@ -190,9 +197,10 @@ def uicleanupcheck(obj: Any, widget: ba.Widget) -> None:
|
|||
widget.add_delete_callback(foobar)
|
||||
|
||||
_ba.app.ui.cleanupchecks.append(
|
||||
UICleanupCheck(obj=weakref.ref(obj),
|
||||
widget=widget,
|
||||
widget_death_time=None))
|
||||
UICleanupCheck(
|
||||
obj=weakref.ref(obj), widget=widget, widget_death_time=None
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def ui_upkeep() -> None:
|
||||
|
|
@ -218,11 +226,12 @@ def ui_upkeep() -> None:
|
|||
# Widget was already dead; complain if its been too long.
|
||||
if now - check.widget_death_time > 5.0:
|
||||
print(
|
||||
'WARNING:', obj,
|
||||
'WARNING:',
|
||||
obj,
|
||||
'is still alive 5 second after its widget died;'
|
||||
' you might have a memory leak.')
|
||||
print_active_refs(obj)
|
||||
|
||||
' you might have a memory leak. See efro.debug module'
|
||||
' for tools to help debug this.',
|
||||
)
|
||||
else:
|
||||
remainingchecks.append(check)
|
||||
ui.cleanupchecks = remainingchecks
|
||||
|
|
|
|||
12
dist/ba_data/python/bacommon/assets.py
vendored
12
dist/ba_data/python/bacommon/assets.py
vendored
|
|
@ -26,6 +26,7 @@ class AssetPackageFlavor(Enum):
|
|||
|
||||
class AssetType(Enum):
|
||||
"""Types for individual assets within a package."""
|
||||
|
||||
TEXTURE = 'texture'
|
||||
CUBE_TEXTURE = 'cube_texture'
|
||||
SOUND = 'sound'
|
||||
|
|
@ -38,8 +39,10 @@ class AssetType(Enum):
|
|||
@dataclass
|
||||
class AssetPackageFlavorManifest:
|
||||
"""A manifest of asset info for a specific flavor of an asset package."""
|
||||
cloudfiles: Annotated[dict[str, str],
|
||||
IOAttrs('cloudfiles')] = field(default_factory=dict)
|
||||
|
||||
cloudfiles: Annotated[dict[str, str], IOAttrs('cloudfiles')] = field(
|
||||
default_factory=dict
|
||||
)
|
||||
|
||||
|
||||
@ioprepped
|
||||
|
|
@ -48,8 +51,9 @@ class AssetPackageBuildState:
|
|||
"""Contains info about an in-progress asset cloud build."""
|
||||
|
||||
# Asset names still being built.
|
||||
in_progress_builds: Annotated[list[str],
|
||||
IOAttrs('b')] = field(default_factory=list)
|
||||
in_progress_builds: Annotated[list[str], IOAttrs('b')] = field(
|
||||
default_factory=list
|
||||
)
|
||||
|
||||
# The initial number of assets needing to be built.
|
||||
initial_build_count: Annotated[int, IOAttrs('c')] = 0
|
||||
|
|
|
|||
46
dist/ba_data/python/bacommon/bacloud.py
vendored
46
dist/ba_data/python/bacommon/bacloud.py
vendored
|
|
@ -21,6 +21,7 @@ BACLOUD_VERSION = 8
|
|||
@dataclass
|
||||
class RequestData:
|
||||
"""Request sent to bacloud server."""
|
||||
|
||||
command: Annotated[str, IOAttrs('c')]
|
||||
token: Annotated[str | None, IOAttrs('t')]
|
||||
payload: Annotated[dict, IOAttrs('p')]
|
||||
|
|
@ -74,23 +75,32 @@ class ResponseData:
|
|||
delay_seconds: Annotated[float, IOAttrs('d', store_default=False)] = 0.0
|
||||
login: Annotated[str | None, IOAttrs('l', store_default=False)] = None
|
||||
logout: Annotated[bool, IOAttrs('lo', store_default=False)] = False
|
||||
dir_manifest: Annotated[str | None,
|
||||
IOAttrs('man', store_default=False)] = None
|
||||
uploads: Annotated[tuple[list[str], str, dict] | None,
|
||||
IOAttrs('u', store_default=False)] = None
|
||||
uploads_inline: Annotated[list[str] | None,
|
||||
IOAttrs('uinl', store_default=False)] = None
|
||||
deletes: Annotated[list[str] | None,
|
||||
IOAttrs('dlt', store_default=False)] = None
|
||||
downloads_inline: Annotated[dict[str, str] | None,
|
||||
IOAttrs('dinl', store_default=False)] = None
|
||||
dir_prune_empty: Annotated[str | None,
|
||||
IOAttrs('dpe', store_default=False)] = None
|
||||
dir_manifest: Annotated[
|
||||
str | None, IOAttrs('man', store_default=False)
|
||||
] = None
|
||||
uploads: Annotated[
|
||||
tuple[list[str], str, dict] | None, IOAttrs('u', store_default=False)
|
||||
] = None
|
||||
uploads_inline: Annotated[
|
||||
list[str] | None, IOAttrs('uinl', store_default=False)
|
||||
] = None
|
||||
deletes: Annotated[
|
||||
list[str] | None, IOAttrs('dlt', store_default=False)
|
||||
] = None
|
||||
downloads_inline: Annotated[
|
||||
dict[str, str] | None, IOAttrs('dinl', store_default=False)
|
||||
] = None
|
||||
dir_prune_empty: Annotated[
|
||||
str | None, IOAttrs('dpe', store_default=False)
|
||||
] = None
|
||||
open_url: Annotated[str | None, IOAttrs('url', store_default=False)] = None
|
||||
input_prompt: Annotated[tuple[str, bool] | None,
|
||||
IOAttrs('inp', store_default=False)] = None
|
||||
end_message: Annotated[str | None,
|
||||
IOAttrs('em', store_default=False)] = None
|
||||
input_prompt: Annotated[
|
||||
tuple[str, bool] | None, IOAttrs('inp', store_default=False)
|
||||
] = None
|
||||
end_message: Annotated[
|
||||
str | None, IOAttrs('em', store_default=False)
|
||||
] = None
|
||||
end_message_end: Annotated[str, IOAttrs('eme', store_default=False)] = '\n'
|
||||
end_command: Annotated[tuple[str, dict] | None,
|
||||
IOAttrs('ec', store_default=False)] = None
|
||||
end_command: Annotated[
|
||||
tuple[str, dict] | None, IOAttrs('ec', store_default=False)
|
||||
] = None
|
||||
|
|
|
|||
6
dist/ba_data/python/bacommon/build.py
vendored
6
dist/ba_data/python/bacommon/build.py
vendored
|
|
@ -22,6 +22,7 @@ class BuildInfoSet:
|
|||
@dataclass
|
||||
class Entry:
|
||||
"""Info about a particular build."""
|
||||
|
||||
filename: Annotated[str, IOAttrs('fname')]
|
||||
size: Annotated[int, IOAttrs('size')]
|
||||
version: Annotated[str, IOAttrs('version')]
|
||||
|
|
@ -29,5 +30,6 @@ class BuildInfoSet:
|
|||
checksum: Annotated[str, IOAttrs('checksum')]
|
||||
createtime: Annotated[datetime.datetime, IOAttrs('createtime')]
|
||||
|
||||
builds: Annotated[list[Entry],
|
||||
IOAttrs('builds')] = field(default_factory=list)
|
||||
builds: Annotated[list[Entry], IOAttrs('builds')] = field(
|
||||
default_factory=list
|
||||
)
|
||||
|
|
|
|||
18
dist/ba_data/python/bacommon/cloud.py
vendored
18
dist/ba_data/python/bacommon/cloud.py
vendored
|
|
@ -44,6 +44,7 @@ class LoginProxyRequestResponse(Response):
|
|||
@dataclass
|
||||
class LoginProxyStateQueryMessage(Message):
|
||||
"""Soo.. how is that login proxy going?"""
|
||||
|
||||
proxyid: Annotated[str, IOAttrs('p')]
|
||||
proxykey: Annotated[str, IOAttrs('k')]
|
||||
|
||||
|
|
@ -59,6 +60,7 @@ class LoginProxyStateQueryResponse(Response):
|
|||
|
||||
class State(Enum):
|
||||
"""States a login-proxy can be in."""
|
||||
|
||||
WAITING = 'waiting'
|
||||
SUCCESS = 'success'
|
||||
FAIL = 'fail'
|
||||
|
|
@ -73,6 +75,7 @@ class LoginProxyStateQueryResponse(Response):
|
|||
@dataclass
|
||||
class LoginProxyCompleteMessage(Message):
|
||||
"""Just so you know, we're done with this proxy."""
|
||||
|
||||
proxyid: Annotated[str, IOAttrs('p')]
|
||||
|
||||
|
||||
|
|
@ -96,6 +99,7 @@ class PingResponse(Response):
|
|||
@dataclass
|
||||
class TestMessage(Message):
|
||||
"""Can I get some of that workspace action?"""
|
||||
|
||||
testfoo: Annotated[int, IOAttrs('f')]
|
||||
|
||||
@classmethod
|
||||
|
|
@ -115,6 +119,7 @@ class TestResponse(Response):
|
|||
@dataclass
|
||||
class WorkspaceFetchState:
|
||||
"""Common state data for a workspace fetch."""
|
||||
|
||||
manifest: Annotated[DirectoryManifest, IOAttrs('m')]
|
||||
iteration: Annotated[int, IOAttrs('i')] = 0
|
||||
total_deletes: Annotated[int, IOAttrs('tdels')] = 0
|
||||
|
|
@ -126,6 +131,7 @@ class WorkspaceFetchState:
|
|||
@dataclass
|
||||
class WorkspaceFetchMessage(Message):
|
||||
"""Can I get some of that workspace action?"""
|
||||
|
||||
workspaceid: Annotated[str, IOAttrs('w')]
|
||||
state: Annotated[WorkspaceFetchState, IOAttrs('s')]
|
||||
|
||||
|
|
@ -140,11 +146,11 @@ class WorkspaceFetchResponse(Response):
|
|||
"""Here's that workspace you asked for, boss."""
|
||||
|
||||
state: Annotated[WorkspaceFetchState, IOAttrs('s')]
|
||||
deletes: Annotated[list[str],
|
||||
IOAttrs('dlt', store_default=False)] = field(
|
||||
default_factory=list)
|
||||
downloads_inline: Annotated[dict[str, bytes],
|
||||
IOAttrs('dinl', store_default=False)] = field(
|
||||
default_factory=dict)
|
||||
deletes: Annotated[list[str], IOAttrs('dlt', store_default=False)] = field(
|
||||
default_factory=list
|
||||
)
|
||||
downloads_inline: Annotated[
|
||||
dict[str, bytes], IOAttrs('dinl', store_default=False)
|
||||
] = field(default_factory=dict)
|
||||
|
||||
done: Annotated[bool, IOAttrs('d')] = False
|
||||
|
|
|
|||
15
dist/ba_data/python/bacommon/net.py
vendored
15
dist/ba_data/python/bacommon/net.py
vendored
|
|
@ -18,6 +18,7 @@ if TYPE_CHECKING:
|
|||
@dataclass
|
||||
class ServerNodeEntry:
|
||||
"""Information about a specific server."""
|
||||
|
||||
zone: Annotated[str, IOAttrs('r')]
|
||||
address: Annotated[str, IOAttrs('a')]
|
||||
port: Annotated[int, IOAttrs('p')]
|
||||
|
|
@ -35,15 +36,16 @@ class ServerNodeQueryResponse:
|
|||
error: Annotated[str | None, IOAttrs('e', store_default=False)] = None
|
||||
|
||||
# The set of servernodes.
|
||||
servers: Annotated[list[ServerNodeEntry],
|
||||
IOAttrs('s', store_default=False)] = field(
|
||||
default_factory=list)
|
||||
servers: Annotated[
|
||||
list[ServerNodeEntry], IOAttrs('s', store_default=False)
|
||||
] = field(default_factory=list)
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class PrivateHostingState:
|
||||
"""Combined state of whether we're hosting, whether we can, etc."""
|
||||
|
||||
unavailable_error: str | None = None
|
||||
party_code: str | None = None
|
||||
tickets_to_host_now: int = 0
|
||||
|
|
@ -55,13 +57,15 @@ class PrivateHostingState:
|
|||
@dataclass
|
||||
class PrivateHostingConfig:
|
||||
"""Config provided when hosting a private party."""
|
||||
|
||||
session_type: str = 'ffa'
|
||||
playlist_name: str = 'Unknown'
|
||||
randomize: bool = False
|
||||
tutorial: bool = False
|
||||
custom_team_names: tuple[str, str] | None = None
|
||||
custom_team_colors: tuple[tuple[float, float, float],
|
||||
tuple[float, float, float]] | None = None
|
||||
custom_team_colors: tuple[
|
||||
tuple[float, float, float], tuple[float, float, float]
|
||||
] | None = None
|
||||
playlist: list[dict[str, Any]] | None = None
|
||||
exit_minutes: float = 120.0
|
||||
exit_minutes_unclean: float = 180.0
|
||||
|
|
@ -72,6 +76,7 @@ class PrivateHostingConfig:
|
|||
@dataclass
|
||||
class PrivatePartyConnectResult:
|
||||
"""Info about a server we get back when connecting."""
|
||||
|
||||
error: str | None = None
|
||||
addr: str | None = None
|
||||
port: int | None = None
|
||||
|
|
|
|||
11
dist/ba_data/python/bacommon/servermanager.py
vendored
11
dist/ba_data/python/bacommon/servermanager.py
vendored
|
|
@ -134,8 +134,9 @@ class ServerConfig:
|
|||
team_names: tuple[str, str] | None = None
|
||||
|
||||
# Team colors (teams mode only).
|
||||
team_colors: tuple[tuple[float, float, float], tuple[float, float,
|
||||
float]] | None = None
|
||||
team_colors: tuple[
|
||||
tuple[float, float, float], tuple[float, float, float]
|
||||
] | None = None
|
||||
|
||||
# (internal) stress-testing mode.
|
||||
stress_test_players: int | None = None
|
||||
|
|
@ -151,11 +152,13 @@ class ServerCommand:
|
|||
@dataclass
|
||||
class StartServerModeCommand(ServerCommand):
|
||||
"""Tells the app to switch into 'server' mode."""
|
||||
|
||||
config: ServerConfig
|
||||
|
||||
|
||||
class ShutdownReason(Enum):
|
||||
"""Reason a server is shutting down."""
|
||||
|
||||
NONE = 'none'
|
||||
RESTARTING = 'restarting'
|
||||
|
||||
|
|
@ -163,6 +166,7 @@ class ShutdownReason(Enum):
|
|||
@dataclass
|
||||
class ShutdownCommand(ServerCommand):
|
||||
"""Tells the server to shut down."""
|
||||
|
||||
reason: ShutdownReason
|
||||
immediate: bool
|
||||
|
||||
|
|
@ -170,6 +174,7 @@ class ShutdownCommand(ServerCommand):
|
|||
@dataclass
|
||||
class ChatMessageCommand(ServerCommand):
|
||||
"""Chat message from the server."""
|
||||
|
||||
message: str
|
||||
clients: list[int] | None
|
||||
|
||||
|
|
@ -177,6 +182,7 @@ class ChatMessageCommand(ServerCommand):
|
|||
@dataclass
|
||||
class ScreenMessageCommand(ServerCommand):
|
||||
"""Screen-message from the server."""
|
||||
|
||||
message: str
|
||||
color: tuple[float, float, float] | None
|
||||
clients: list[int] | None
|
||||
|
|
@ -190,5 +196,6 @@ class ClientListCommand(ServerCommand):
|
|||
@dataclass
|
||||
class KickCommand(ServerCommand):
|
||||
"""Kick a client."""
|
||||
|
||||
client_id: int
|
||||
ban_time: int | None
|
||||
|
|
|
|||
20
dist/ba_data/python/bacommon/transfer.py
vendored
20
dist/ba_data/python/bacommon/transfer.py
vendored
|
|
@ -19,6 +19,7 @@ if TYPE_CHECKING:
|
|||
@dataclass
|
||||
class DirectoryManifestFile:
|
||||
"""Describes metadata and hashes for a file in a manifest."""
|
||||
|
||||
filehash: Annotated[str, IOAttrs('h')]
|
||||
filesize: Annotated[int, IOAttrs('s')]
|
||||
|
||||
|
|
@ -27,6 +28,7 @@ class DirectoryManifestFile:
|
|||
@dataclass
|
||||
class DirectoryManifest:
|
||||
"""Contains a summary of files in a directory."""
|
||||
|
||||
files: Annotated[dict[str, DirectoryManifestFile], IOAttrs('f')]
|
||||
|
||||
_empty_hash: str | None = None
|
||||
|
|
@ -48,7 +50,7 @@ class DirectoryManifest:
|
|||
assert fullname.startswith(pathstr)
|
||||
# Make sure we end up with forward slashes no matter
|
||||
# what the os.* stuff above here was using.
|
||||
paths.append(Path(fullname[len(pathstr) + 1:]).as_posix())
|
||||
paths.append(Path(fullname[len(pathstr) + 1 :]).as_posix())
|
||||
elif path.exists():
|
||||
# Just return a single file entry if path is not a dir.
|
||||
paths.append(path.as_posix())
|
||||
|
|
@ -62,9 +64,12 @@ class DirectoryManifest:
|
|||
filebytes = infile.read()
|
||||
filesize = len(filebytes)
|
||||
sha.update(filebytes)
|
||||
return (filepath,
|
||||
DirectoryManifestFile(filehash=sha.hexdigest(),
|
||||
filesize=filesize))
|
||||
return (
|
||||
filepath,
|
||||
DirectoryManifestFile(
|
||||
filehash=sha.hexdigest(), filesize=filesize
|
||||
),
|
||||
)
|
||||
|
||||
# Now use all procs to hash the files efficiently.
|
||||
cpus = os.cpu_count()
|
||||
|
|
@ -76,13 +81,15 @@ class DirectoryManifest:
|
|||
def validate(self) -> None:
|
||||
"""Log any odd data in the manifest; for debugging."""
|
||||
import logging
|
||||
|
||||
for fpath, _fentry in self.files.items():
|
||||
# We want to be dealing in only forward slashes; make sure
|
||||
# that's the case (wondering if we'll ever see backslashes
|
||||
# for escape purposes).
|
||||
if '\\' in fpath:
|
||||
logging.exception("Found unusual path in manifest: '%s'.",
|
||||
fpath)
|
||||
logging.exception(
|
||||
"Found unusual path in manifest: '%s'.", fpath
|
||||
)
|
||||
break # 1 error is enough for now.
|
||||
|
||||
@classmethod
|
||||
|
|
@ -90,6 +97,7 @@ class DirectoryManifest:
|
|||
"""Return the hash for an empty file."""
|
||||
if cls._empty_hash is None:
|
||||
import hashlib
|
||||
|
||||
sha = hashlib.sha256()
|
||||
cls._empty_hash = sha.hexdigest()
|
||||
return cls._empty_hash
|
||||
|
|
|
|||
76
dist/ba_data/python/bastd/activity/coopjoin.py
vendored
76
dist/ba_data/python/bastd/activity/coopjoin.py
vendored
|
|
@ -27,19 +27,23 @@ class CoopJoinActivity(JoinActivity):
|
|||
def on_transition_in(self) -> None:
|
||||
from bastd.actor.controlsguide import ControlsGuide
|
||||
from bastd.actor.text import Text
|
||||
|
||||
super().on_transition_in()
|
||||
assert isinstance(self.session, ba.CoopSession)
|
||||
assert self.session.campaign
|
||||
Text(self.session.campaign.getlevel(
|
||||
self.session.campaign_level_name).displayname,
|
||||
scale=1.3,
|
||||
h_attach=Text.HAttach.CENTER,
|
||||
h_align=Text.HAlign.CENTER,
|
||||
v_attach=Text.VAttach.TOP,
|
||||
transition=Text.Transition.FADE_IN,
|
||||
transition_delay=4.0,
|
||||
color=(1, 1, 1, 0.6),
|
||||
position=(0, -95)).autoretain()
|
||||
Text(
|
||||
self.session.campaign.getlevel(
|
||||
self.session.campaign_level_name
|
||||
).displayname,
|
||||
scale=1.3,
|
||||
h_attach=Text.HAttach.CENTER,
|
||||
h_align=Text.HAlign.CENTER,
|
||||
v_attach=Text.VAttach.TOP,
|
||||
transition=Text.Transition.FADE_IN,
|
||||
transition_delay=4.0,
|
||||
color=(1, 1, 1, 0.6),
|
||||
position=(0, -95),
|
||||
).autoretain()
|
||||
ControlsGuide(delay=1.0).autoretain()
|
||||
|
||||
ba.pushcall(self._show_remaining_achievements)
|
||||
|
|
@ -60,30 +64,34 @@ 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_level_name)
|
||||
levelname = (
|
||||
self.session.campaign.name + ':' + self.session.campaign_level_name
|
||||
)
|
||||
ts_h_offs = 60
|
||||
|
||||
if not (ba.app.demo_mode or ba.app.arcade_mode):
|
||||
achievements = [
|
||||
a for a in ba.app.ach.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)
|
||||
achievements = [a for a in achievements if not a.complete]
|
||||
vrmode = ba.app.vr_mode
|
||||
if have_achievements:
|
||||
Text(ba.Lstr(resource='achievementsRemainingText'),
|
||||
host_only=True,
|
||||
position=(ts_h_offs - 10, vpos),
|
||||
transition=Text.Transition.FADE_IN,
|
||||
scale=1.1 * 0.76,
|
||||
h_attach=Text.HAttach.LEFT,
|
||||
v_attach=Text.VAttach.TOP,
|
||||
color=(1, 1, 1.2, 1) if vrmode else (0.8, 0.8, 1, 1),
|
||||
shadow=1.0,
|
||||
flatness=1.0 if vrmode else 0.6,
|
||||
transition_delay=delay).autoretain()
|
||||
Text(
|
||||
ba.Lstr(resource='achievementsRemainingText'),
|
||||
host_only=True,
|
||||
position=(ts_h_offs - 10, vpos),
|
||||
transition=Text.Transition.FADE_IN,
|
||||
scale=1.1 * 0.76,
|
||||
h_attach=Text.HAttach.LEFT,
|
||||
v_attach=Text.VAttach.TOP,
|
||||
color=(1, 1, 1.2, 1) if vrmode else (0.8, 0.8, 1, 1),
|
||||
shadow=1.0,
|
||||
flatness=1.0 if vrmode else 0.6,
|
||||
transition_delay=delay,
|
||||
).autoretain()
|
||||
hval = ts_h_offs + 50
|
||||
vpos -= 35
|
||||
for ach in achievements:
|
||||
|
|
@ -91,12 +99,14 @@ class CoopJoinActivity(JoinActivity):
|
|||
ach.create_display(hval, vpos, delay, style='in_game')
|
||||
vpos -= 55
|
||||
if not achievements:
|
||||
Text(ba.Lstr(resource='noAchievementsRemainingText'),
|
||||
host_only=True,
|
||||
position=(ts_h_offs + 15, vpos + 10),
|
||||
transition=Text.Transition.FADE_IN,
|
||||
scale=0.7,
|
||||
h_attach=Text.HAttach.LEFT,
|
||||
v_attach=Text.VAttach.TOP,
|
||||
color=(1, 1, 1, 0.5),
|
||||
transition_delay=delay + 0.5).autoretain()
|
||||
Text(
|
||||
ba.Lstr(resource='noAchievementsRemainingText'),
|
||||
host_only=True,
|
||||
position=(ts_h_offs + 15, vpos + 10),
|
||||
transition=Text.Transition.FADE_IN,
|
||||
scale=0.7,
|
||||
h_attach=Text.HAttach.LEFT,
|
||||
v_attach=Text.VAttach.TOP,
|
||||
color=(1, 1, 1, 0.5),
|
||||
transition_delay=delay + 0.5,
|
||||
).autoretain()
|
||||
|
|
|
|||
1501
dist/ba_data/python/bastd/activity/coopscore.py
vendored
1501
dist/ba_data/python/bastd/activity/coopscore.py
vendored
File diff suppressed because it is too large
Load diff
18
dist/ba_data/python/bastd/activity/drawscore.py
vendored
18
dist/ba_data/python/bastd/activity/drawscore.py
vendored
|
|
@ -22,13 +22,15 @@ class DrawScoreScreenActivity(MultiTeamScoreScreenActivity):
|
|||
def on_begin(self) -> None:
|
||||
ba.set_analytics_screen('Draw Score Screen')
|
||||
super().on_begin()
|
||||
ZoomText(ba.Lstr(resource='drawText'),
|
||||
position=(0, 0),
|
||||
maxwidth=400,
|
||||
shiftposition=(-220, 0),
|
||||
shiftdelay=2.0,
|
||||
flash=False,
|
||||
trail=False,
|
||||
jitter=1.0).autoretain()
|
||||
ZoomText(
|
||||
ba.Lstr(resource='drawText'),
|
||||
position=(0, 0),
|
||||
maxwidth=400,
|
||||
shiftposition=(-220, 0),
|
||||
shiftdelay=2.0,
|
||||
flash=False,
|
||||
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_raw.get('results', None))
|
||||
|
|
|
|||
177
dist/ba_data/python/bastd/activity/dualteamscore.py
vendored
177
dist/ba_data/python/bastd/activity/dualteamscore.py
vendored
|
|
@ -37,91 +37,132 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
|
|||
session = self.session
|
||||
assert isinstance(session, ba.MultiTeamSession)
|
||||
if ba.app.lang.get_resource('bestOfUseFirstToInstead'):
|
||||
best_txt = ba.Lstr(resource='firstToSeriesText',
|
||||
subs=[('${COUNT}',
|
||||
str(session.get_series_length() / 2 + 1))
|
||||
])
|
||||
best_txt = ba.Lstr(
|
||||
resource='firstToSeriesText',
|
||||
subs=[('${COUNT}', str(session.get_series_length() / 2 + 1))],
|
||||
)
|
||||
else:
|
||||
best_txt = ba.Lstr(resource='bestOfSeriesText',
|
||||
subs=[('${COUNT}',
|
||||
str(session.get_series_length()))])
|
||||
best_txt = ba.Lstr(
|
||||
resource='bestOfSeriesText',
|
||||
subs=[('${COUNT}', str(session.get_series_length()))],
|
||||
)
|
||||
|
||||
ZoomText(best_txt,
|
||||
position=(0, 175),
|
||||
shiftposition=(-250, 175),
|
||||
shiftdelay=2.5,
|
||||
flash=False,
|
||||
trail=False,
|
||||
h_align='center',
|
||||
scale=0.25,
|
||||
color=(0.5, 0.5, 0.5, 1.0),
|
||||
jitter=3.0).autoretain()
|
||||
ZoomText(
|
||||
best_txt,
|
||||
position=(0, 175),
|
||||
shiftposition=(-250, 175),
|
||||
shiftdelay=2.5,
|
||||
flash=False,
|
||||
trail=False,
|
||||
h_align='center',
|
||||
scale=0.25,
|
||||
color=(0.5, 0.5, 0.5, 1.0),
|
||||
jitter=3.0,
|
||||
).autoretain()
|
||||
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._winner)
|
||||
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._winner
|
||||
delay = 0.2
|
||||
if scored:
|
||||
delay = 1.2
|
||||
ba.timer(
|
||||
i * 0.150 + 0.2,
|
||||
ba.WeakCall(self._show_team_old_score, vval - i * height,
|
||||
team, shift_time - (i * 0.15 + 0.2)))
|
||||
ba.timer(i * 0.15 + 1.5,
|
||||
ba.Call(ba.playsound, self._score_display_sound))
|
||||
ba.WeakCall(
|
||||
self._show_team_old_score,
|
||||
vval - i * height,
|
||||
team,
|
||||
shift_time - (i * 0.15 + 0.2),
|
||||
),
|
||||
)
|
||||
ba.timer(
|
||||
i * 0.15 + 1.5,
|
||||
ba.Call(ba.playsound, self._score_display_sound),
|
||||
)
|
||||
|
||||
ba.timer(
|
||||
i * 0.150 + delay,
|
||||
ba.WeakCall(self._show_team_score, vval - i * height, team,
|
||||
scored, i * 0.2 + 0.1,
|
||||
shift_time - (i * 0.15 + delay)))
|
||||
ba.WeakCall(
|
||||
self._show_team_score,
|
||||
vval - i * height,
|
||||
team,
|
||||
scored,
|
||||
i * 0.2 + 0.1,
|
||||
shift_time - (i * 0.15 + delay),
|
||||
),
|
||||
)
|
||||
i += 1
|
||||
self.show_player_scores()
|
||||
|
||||
def _show_team_name(self, pos_v: float, team: ba.SessionTeam,
|
||||
kill_delay: float, shiftdelay: float) -> None:
|
||||
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),
|
||||
shiftdelay=shiftdelay,
|
||||
flash=False,
|
||||
trail=False,
|
||||
h_align='right',
|
||||
maxwidth=300,
|
||||
color=team.color,
|
||||
jitter=1.0).autoretain()
|
||||
ZoomText(
|
||||
ba.Lstr(value='${A}:', subs=[('${A}', team.name)]),
|
||||
position=(100, pos_v),
|
||||
shiftposition=(-150, pos_v),
|
||||
shiftdelay=shiftdelay,
|
||||
flash=False,
|
||||
trail=False,
|
||||
h_align='right',
|
||||
maxwidth=300,
|
||||
color=team.color,
|
||||
jitter=1.0,
|
||||
).autoretain()
|
||||
|
||||
def _show_team_old_score(self, pos_v: float, sessionteam: ba.SessionTeam,
|
||||
shiftdelay: float) -> None:
|
||||
ZoomText(str(sessionteam.customdata['score'] - 1),
|
||||
position=(150, pos_v),
|
||||
maxwidth=100,
|
||||
color=(0.6, 0.6, 0.7),
|
||||
shiftposition=(-100, pos_v),
|
||||
shiftdelay=shiftdelay,
|
||||
flash=False,
|
||||
trail=False,
|
||||
lifespan=1.0,
|
||||
h_align='left',
|
||||
jitter=1.0).autoretain()
|
||||
def _show_team_old_score(
|
||||
self, pos_v: float, sessionteam: ba.SessionTeam, shiftdelay: float
|
||||
) -> None:
|
||||
ZoomText(
|
||||
str(sessionteam.customdata['score'] - 1),
|
||||
position=(150, pos_v),
|
||||
maxwidth=100,
|
||||
color=(0.6, 0.6, 0.7),
|
||||
shiftposition=(-100, pos_v),
|
||||
shiftdelay=shiftdelay,
|
||||
flash=False,
|
||||
trail=False,
|
||||
lifespan=1.0,
|
||||
h_align='left',
|
||||
jitter=1.0,
|
||||
).autoretain()
|
||||
|
||||
def _show_team_score(self, pos_v: float, sessionteam: ba.SessionTeam,
|
||||
scored: bool, kill_delay: float,
|
||||
shiftdelay: float) -> None:
|
||||
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),
|
||||
shiftposition=(-100, pos_v),
|
||||
shiftdelay=shiftdelay,
|
||||
flash=scored,
|
||||
trail=scored,
|
||||
h_align='left',
|
||||
jitter=1.0,
|
||||
trailcolor=(1, 0.8, 0.0, 0)).autoretain()
|
||||
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),
|
||||
shiftposition=(-100, pos_v),
|
||||
shiftdelay=shiftdelay,
|
||||
flash=scored,
|
||||
trail=scored,
|
||||
h_align='left',
|
||||
jitter=1.0,
|
||||
trailcolor=(1, 0.8, 0.0, 0),
|
||||
).autoretain()
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
|
|||
# pylint: disable=too-many-statements
|
||||
from bastd.actor.text import Text
|
||||
from bastd.actor.image import Image
|
||||
|
||||
ba.set_analytics_screen('FreeForAll Score Screen')
|
||||
super().on_begin()
|
||||
|
||||
|
|
@ -45,14 +46,17 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
|
|||
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.sessionteam.customdata['score'],
|
||||
p.team.sessionteam.customdata['score'],
|
||||
p.getname(full=True),
|
||||
))
|
||||
player_order.sort(
|
||||
reverse=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
|
||||
|
|
@ -66,30 +70,36 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
|
|||
ba.timer(0.3, ba.Call(ba.playsound, self._score_display_sound))
|
||||
results = self.settings_raw['results']
|
||||
assert isinstance(results, ba.GameResults)
|
||||
self.show_player_scores(delay=0.001,
|
||||
results=results,
|
||||
scale=1.2,
|
||||
x_offset=-110.0)
|
||||
self.show_player_scores(
|
||||
delay=0.001, results=results, scale=1.2, x_offset=-110.0
|
||||
)
|
||||
|
||||
sound_times: set[float] = set()
|
||||
|
||||
def _scoretxt(text: str,
|
||||
x_offs: float,
|
||||
y_offs: float,
|
||||
highlight: bool,
|
||||
delay: float,
|
||||
extrascale: float,
|
||||
flash: bool = False) -> Text:
|
||||
return Text(text,
|
||||
position=(ts_h_offs + x_offs * scale,
|
||||
y_base + (y_offs + v_offs + 2.0) * scale),
|
||||
scale=scale * extrascale,
|
||||
color=((1.0, 0.7, 0.3, 1.0) if highlight else
|
||||
(0.7, 0.7, 0.7, 0.7)),
|
||||
h_align=Text.HAlign.RIGHT,
|
||||
transition=Text.Transition.IN_LEFT,
|
||||
transition_delay=tdelay + delay,
|
||||
flash=flash).autoretain()
|
||||
def _scoretxt(
|
||||
text: str,
|
||||
x_offs: float,
|
||||
y_offs: float,
|
||||
highlight: bool,
|
||||
delay: float,
|
||||
extrascale: float,
|
||||
flash: bool = False,
|
||||
) -> Text:
|
||||
return Text(
|
||||
text,
|
||||
position=(
|
||||
ts_h_offs + x_offs * scale,
|
||||
y_base + (y_offs + v_offs + 2.0) * scale,
|
||||
),
|
||||
scale=scale * extrascale,
|
||||
color=(
|
||||
(1.0, 0.7, 0.3, 1.0) if highlight else (0.7, 0.7, 0.7, 0.7)
|
||||
),
|
||||
h_align=Text.HAlign.RIGHT,
|
||||
transition=Text.Transition.IN_LEFT,
|
||||
transition_delay=tdelay + delay,
|
||||
flash=flash,
|
||||
).autoretain()
|
||||
|
||||
v_offs -= spacing
|
||||
slide_amt = 0.0
|
||||
|
|
@ -98,16 +108,21 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
|
|||
|
||||
session = self.session
|
||||
assert isinstance(session, ba.FreeForAllSession)
|
||||
title = Text(ba.Lstr(resource='firstToSeriesText',
|
||||
subs=[('${COUNT}',
|
||||
str(session.get_ffa_series_length()))]),
|
||||
scale=1.05 * scale,
|
||||
position=(ts_h_offs - 0.0 * scale,
|
||||
y_base + (v_offs + 50.0) * scale),
|
||||
h_align=Text.HAlign.CENTER,
|
||||
color=(0.5, 0.5, 0.5, 0.5),
|
||||
transition=Text.Transition.IN_LEFT,
|
||||
transition_delay=tdelay).autoretain()
|
||||
title = Text(
|
||||
ba.Lstr(
|
||||
resource='firstToSeriesText',
|
||||
subs=[('${COUNT}', str(session.get_ffa_series_length()))],
|
||||
),
|
||||
scale=1.05 * scale,
|
||||
position=(
|
||||
ts_h_offs - 0.0 * scale,
|
||||
y_base + (v_offs + 50.0) * scale,
|
||||
),
|
||||
h_align=Text.HAlign.CENTER,
|
||||
color=(0.5, 0.5, 0.5, 0.5),
|
||||
transition=Text.Transition.IN_LEFT,
|
||||
transition_delay=tdelay,
|
||||
).autoretain()
|
||||
|
||||
v_offs -= 25
|
||||
v_offs_start = v_offs
|
||||
|
|
@ -115,152 +130,239 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
|
|||
ba.timer(
|
||||
tdelay + delay3,
|
||||
ba.WeakCall(
|
||||
self._safe_animate, title.position_combine, 'input0', {
|
||||
self._safe_animate,
|
||||
title.position_combine,
|
||||
'input0',
|
||||
{
|
||||
0.0: ts_h_offs - 0.0 * scale,
|
||||
transtime2: ts_h_offs - (0.0 + slide_amt) * scale
|
||||
}))
|
||||
transtime2: ts_h_offs - (0.0 + slide_amt) * scale,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
for i, player in enumerate(player_order_prev):
|
||||
v_offs_2 = v_offs_start - spacing * (player_order.index(player))
|
||||
ba.timer(tdelay + 0.3,
|
||||
ba.Call(ba.playsound, self._score_display_sound_small))
|
||||
ba.timer(
|
||||
tdelay + 0.3,
|
||||
ba.Call(ba.playsound, self._score_display_sound_small),
|
||||
)
|
||||
if order_change:
|
||||
ba.timer(tdelay + delay2 + 0.1,
|
||||
ba.Call(ba.playsound, self._cymbal_sound))
|
||||
img = Image(player.get_icon(),
|
||||
position=(ts_h_offs - 72.0 * scale,
|
||||
y_base + (v_offs + 15.0) * scale),
|
||||
scale=(30.0 * scale, 30.0 * scale),
|
||||
transition=Image.Transition.IN_LEFT,
|
||||
transition_delay=tdelay).autoretain()
|
||||
ba.timer(
|
||||
tdelay + delay2 + 0.1,
|
||||
ba.Call(ba.playsound, self._cymbal_sound),
|
||||
)
|
||||
img = Image(
|
||||
player.get_icon(),
|
||||
position=(
|
||||
ts_h_offs - 72.0 * scale,
|
||||
y_base + (v_offs + 15.0) * scale,
|
||||
),
|
||||
scale=(30.0 * scale, 30.0 * scale),
|
||||
transition=Image.Transition.IN_LEFT,
|
||||
transition_delay=tdelay,
|
||||
).autoretain()
|
||||
ba.timer(
|
||||
tdelay + delay2,
|
||||
ba.WeakCall(
|
||||
self._safe_animate, img.position_combine, 'input1', {
|
||||
self._safe_animate,
|
||||
img.position_combine,
|
||||
'input1',
|
||||
{
|
||||
0: y_base + (v_offs + 15.0) * scale,
|
||||
transtime: y_base + (v_offs_2 + 15.0) * scale
|
||||
}))
|
||||
transtime: y_base + (v_offs_2 + 15.0) * scale,
|
||||
},
|
||||
),
|
||||
)
|
||||
ba.timer(
|
||||
tdelay + delay3,
|
||||
ba.WeakCall(
|
||||
self._safe_animate, img.position_combine, 'input0', {
|
||||
self._safe_animate,
|
||||
img.position_combine,
|
||||
'input0',
|
||||
{
|
||||
0: ts_h_offs - 72.0 * scale,
|
||||
transtime2: ts_h_offs - (72.0 + slide_amt) * scale
|
||||
}))
|
||||
txt = Text(ba.Lstr(value=player.getname(full=True)),
|
||||
maxwidth=130.0,
|
||||
scale=0.75 * scale,
|
||||
position=(ts_h_offs - 50.0 * scale,
|
||||
y_base + (v_offs + 15.0) * scale),
|
||||
h_align=Text.HAlign.LEFT,
|
||||
v_align=Text.VAlign.CENTER,
|
||||
color=ba.safecolor(player.team.color + (1, )),
|
||||
transition=Text.Transition.IN_LEFT,
|
||||
transition_delay=tdelay).autoretain()
|
||||
transtime2: ts_h_offs - (72.0 + slide_amt) * scale,
|
||||
},
|
||||
),
|
||||
)
|
||||
txt = Text(
|
||||
ba.Lstr(value=player.getname(full=True)),
|
||||
maxwidth=130.0,
|
||||
scale=0.75 * scale,
|
||||
position=(
|
||||
ts_h_offs - 50.0 * scale,
|
||||
y_base + (v_offs + 15.0) * scale,
|
||||
),
|
||||
h_align=Text.HAlign.LEFT,
|
||||
v_align=Text.VAlign.CENTER,
|
||||
color=ba.safecolor(player.team.color + (1,)),
|
||||
transition=Text.Transition.IN_LEFT,
|
||||
transition_delay=tdelay,
|
||||
).autoretain()
|
||||
ba.timer(
|
||||
tdelay + delay2,
|
||||
ba.WeakCall(
|
||||
self._safe_animate, txt.position_combine, 'input1', {
|
||||
self._safe_animate,
|
||||
txt.position_combine,
|
||||
'input1',
|
||||
{
|
||||
0: y_base + (v_offs + 15.0) * scale,
|
||||
transtime: y_base + (v_offs_2 + 15.0) * scale
|
||||
}))
|
||||
transtime: y_base + (v_offs_2 + 15.0) * scale,
|
||||
},
|
||||
),
|
||||
)
|
||||
ba.timer(
|
||||
tdelay + delay3,
|
||||
ba.WeakCall(
|
||||
self._safe_animate, txt.position_combine, 'input0', {
|
||||
self._safe_animate,
|
||||
txt.position_combine,
|
||||
'input0',
|
||||
{
|
||||
0: ts_h_offs - 50.0 * scale,
|
||||
transtime2: ts_h_offs - (50.0 + slide_amt) * scale
|
||||
}))
|
||||
transtime2: ts_h_offs - (50.0 + slide_amt) * scale,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
txt_num = Text('#' + str(i + 1),
|
||||
scale=0.55 * scale,
|
||||
position=(ts_h_offs - 95.0 * scale,
|
||||
y_base + (v_offs + 8.0) * scale),
|
||||
h_align=Text.HAlign.RIGHT,
|
||||
color=(0.6, 0.6, 0.6, 0.6),
|
||||
transition=Text.Transition.IN_LEFT,
|
||||
transition_delay=tdelay).autoretain()
|
||||
txt_num = Text(
|
||||
'#' + str(i + 1),
|
||||
scale=0.55 * scale,
|
||||
position=(
|
||||
ts_h_offs - 95.0 * scale,
|
||||
y_base + (v_offs + 8.0) * scale,
|
||||
),
|
||||
h_align=Text.HAlign.RIGHT,
|
||||
color=(0.6, 0.6, 0.6, 0.6),
|
||||
transition=Text.Transition.IN_LEFT,
|
||||
transition_delay=tdelay,
|
||||
).autoretain()
|
||||
ba.timer(
|
||||
tdelay + delay3,
|
||||
ba.WeakCall(
|
||||
self._safe_animate, txt_num.position_combine, 'input0', {
|
||||
self._safe_animate,
|
||||
txt_num.position_combine,
|
||||
'input0',
|
||||
{
|
||||
0: ts_h_offs - 95.0 * scale,
|
||||
transtime2: ts_h_offs - (95.0 + slide_amt) * scale
|
||||
}))
|
||||
transtime2: ts_h_offs - (95.0 + slide_amt) * scale,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
s_txt = _scoretxt(
|
||||
str(player.team.sessionteam.customdata['previous_score']), 80,
|
||||
0, False, 0, 1.0)
|
||||
str(player.team.sessionteam.customdata['previous_score']),
|
||||
80,
|
||||
0,
|
||||
False,
|
||||
0,
|
||||
1.0,
|
||||
)
|
||||
ba.timer(
|
||||
tdelay + delay2,
|
||||
ba.WeakCall(
|
||||
self._safe_animate, s_txt.position_combine, 'input1', {
|
||||
self._safe_animate,
|
||||
s_txt.position_combine,
|
||||
'input1',
|
||||
{
|
||||
0: y_base + (v_offs + 2.0) * scale,
|
||||
transtime: y_base + (v_offs_2 + 2.0) * scale
|
||||
}))
|
||||
transtime: y_base + (v_offs_2 + 2.0) * scale,
|
||||
},
|
||||
),
|
||||
)
|
||||
ba.timer(
|
||||
tdelay + delay3,
|
||||
ba.WeakCall(
|
||||
self._safe_animate, s_txt.position_combine, 'input0', {
|
||||
self._safe_animate,
|
||||
s_txt.position_combine,
|
||||
'input0',
|
||||
{
|
||||
0: ts_h_offs + 80.0 * scale,
|
||||
transtime2: ts_h_offs + (80.0 - slide_amt) * scale
|
||||
}))
|
||||
transtime2: ts_h_offs + (80.0 - slide_amt) * scale,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
score_change = (
|
||||
player.team.sessionteam.customdata['score'] -
|
||||
player.team.sessionteam.customdata['previous_score'])
|
||||
player.team.sessionteam.customdata['score']
|
||||
- player.team.sessionteam.customdata['previous_score']
|
||||
)
|
||||
if score_change > 0:
|
||||
xval = 113
|
||||
yval = 3.0
|
||||
s_txt_2 = _scoretxt('+' + str(score_change),
|
||||
xval,
|
||||
yval,
|
||||
True,
|
||||
0,
|
||||
0.7,
|
||||
flash=True)
|
||||
s_txt_2 = _scoretxt(
|
||||
'+' + str(score_change),
|
||||
xval,
|
||||
yval,
|
||||
True,
|
||||
0,
|
||||
0.7,
|
||||
flash=True,
|
||||
)
|
||||
ba.timer(
|
||||
tdelay + delay2,
|
||||
ba.WeakCall(
|
||||
self._safe_animate, s_txt_2.position_combine, 'input1',
|
||||
self._safe_animate,
|
||||
s_txt_2.position_combine,
|
||||
'input1',
|
||||
{
|
||||
0: y_base + (v_offs + yval + 2.0) * scale,
|
||||
transtime: y_base + (v_offs_2 + yval + 2.0) * scale
|
||||
}))
|
||||
transtime: y_base + (v_offs_2 + yval + 2.0) * scale,
|
||||
},
|
||||
),
|
||||
)
|
||||
ba.timer(
|
||||
tdelay + delay3,
|
||||
ba.WeakCall(
|
||||
self._safe_animate, s_txt_2.position_combine, 'input0',
|
||||
self._safe_animate,
|
||||
s_txt_2.position_combine,
|
||||
'input0',
|
||||
{
|
||||
0: ts_h_offs + xval * scale,
|
||||
transtime2: ts_h_offs + (xval - slide_amt) * scale
|
||||
}))
|
||||
transtime2: ts_h_offs + (xval - slide_amt) * scale,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
def _safesetattr(node: ba.Node | None, attr: str,
|
||||
value: Any) -> None:
|
||||
def _safesetattr(
|
||||
node: ba.Node | None, attr: str, value: Any
|
||||
) -> None:
|
||||
if node:
|
||||
setattr(node, attr, value)
|
||||
|
||||
ba.timer(
|
||||
tdelay + delay1,
|
||||
ba.Call(_safesetattr, s_txt.node, 'color', (1, 1, 1, 1)))
|
||||
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.sessionteam.
|
||||
customdata['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)
|
||||
ba.timer(
|
||||
tfin,
|
||||
ba.Call(ba.playsound,
|
||||
self._score_display_sound_small))
|
||||
ba.Call(
|
||||
ba.playsound, self._score_display_sound_small
|
||||
),
|
||||
)
|
||||
v_offs -= spacing
|
||||
|
||||
def _safe_animate(self, node: ba.Node | None, attr: str,
|
||||
keys: dict[float, float]) -> None:
|
||||
def _safe_animate(
|
||||
self, node: ba.Node | None, attr: str, keys: dict[float, float]
|
||||
) -> None:
|
||||
"""Run an animation on a node if the node still exists."""
|
||||
if node:
|
||||
ba.animate(node, attr, keys)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ class MultiTeamJoinActivity(JoinActivity):
|
|||
def on_transition_in(self) -> None:
|
||||
from bastd.actor.controlsguide import ControlsGuide
|
||||
from ba import DualTeamSession
|
||||
|
||||
super().on_transition_in()
|
||||
ControlsGuide(delay=1.0).autoretain()
|
||||
|
||||
|
|
@ -31,50 +32,62 @@ class MultiTeamJoinActivity(JoinActivity):
|
|||
assert isinstance(session, ba.MultiTeamSession)
|
||||
|
||||
# Show info about the next up game.
|
||||
self._next_up_text = Text(ba.Lstr(
|
||||
value='${1} ${2}',
|
||||
subs=[('${1}', ba.Lstr(resource='upFirstText')),
|
||||
('${2}', session.get_next_game_description())]),
|
||||
h_attach=Text.HAttach.CENTER,
|
||||
scale=0.7,
|
||||
v_attach=Text.VAttach.TOP,
|
||||
h_align=Text.HAlign.CENTER,
|
||||
position=(0, -70),
|
||||
flash=False,
|
||||
color=(0.5, 0.5, 0.5, 1.0),
|
||||
transition=Text.Transition.FADE_IN,
|
||||
transition_delay=5.0)
|
||||
self._next_up_text = Text(
|
||||
ba.Lstr(
|
||||
value='${1} ${2}',
|
||||
subs=[
|
||||
('${1}', ba.Lstr(resource='upFirstText')),
|
||||
('${2}', session.get_next_game_description()),
|
||||
],
|
||||
),
|
||||
h_attach=Text.HAttach.CENTER,
|
||||
scale=0.7,
|
||||
v_attach=Text.VAttach.TOP,
|
||||
h_align=Text.HAlign.CENTER,
|
||||
position=(0, -70),
|
||||
flash=False,
|
||||
color=(0.5, 0.5, 0.5, 1.0),
|
||||
transition=Text.Transition.FADE_IN,
|
||||
transition_delay=5.0,
|
||||
)
|
||||
|
||||
# In teams mode, show our two team names.
|
||||
# FIXME: Lobby should handle this.
|
||||
if isinstance(ba.getsession(), DualTeamSession):
|
||||
team_names = [team.name for team in ba.getsession().sessionteams]
|
||||
team_colors = [
|
||||
tuple(team.color) + (0.5, )
|
||||
tuple(team.color) + (0.5,)
|
||||
for team in ba.getsession().sessionteams
|
||||
]
|
||||
if len(team_names) == 2:
|
||||
for i in range(2):
|
||||
Text(team_names[i],
|
||||
scale=0.7,
|
||||
h_attach=Text.HAttach.CENTER,
|
||||
v_attach=Text.VAttach.TOP,
|
||||
h_align=Text.HAlign.CENTER,
|
||||
position=(-200 + 350 * i, -100),
|
||||
color=team_colors[i],
|
||||
transition=Text.Transition.FADE_IN).autoretain()
|
||||
Text(
|
||||
team_names[i],
|
||||
scale=0.7,
|
||||
h_attach=Text.HAttach.CENTER,
|
||||
v_attach=Text.VAttach.TOP,
|
||||
h_align=Text.HAlign.CENTER,
|
||||
position=(-200 + 350 * i, -100),
|
||||
color=team_colors[i],
|
||||
transition=Text.Transition.FADE_IN,
|
||||
).autoretain()
|
||||
|
||||
Text(ba.Lstr(resource='mustInviteFriendsText',
|
||||
subs=[('${GATHER}',
|
||||
ba.Lstr(resource='gatherWindow.titleText'))]),
|
||||
h_attach=Text.HAttach.CENTER,
|
||||
scale=0.8,
|
||||
host_only=True,
|
||||
v_attach=Text.VAttach.CENTER,
|
||||
h_align=Text.HAlign.CENTER,
|
||||
position=(0, 0),
|
||||
flash=False,
|
||||
color=(0, 1, 0, 1.0),
|
||||
transition=Text.Transition.FADE_IN,
|
||||
transition_delay=2.0,
|
||||
transition_out_delay=7.0).autoretain()
|
||||
Text(
|
||||
ba.Lstr(
|
||||
resource='mustInviteFriendsText',
|
||||
subs=[
|
||||
('${GATHER}', ba.Lstr(resource='gatherWindow.titleText'))
|
||||
],
|
||||
),
|
||||
h_attach=Text.HAttach.CENTER,
|
||||
scale=0.8,
|
||||
host_only=True,
|
||||
v_attach=Text.VAttach.CENTER,
|
||||
h_align=Text.HAlign.CENTER,
|
||||
position=(0, 0),
|
||||
flash=False,
|
||||
color=(0, 1, 0, 1.0),
|
||||
transition=Text.Transition.FADE_IN,
|
||||
transition_delay=2.0,
|
||||
transition_out_delay=7.0,
|
||||
).autoretain()
|
||||
|
|
|
|||
235
dist/ba_data/python/bastd/activity/multiteamscore.py
vendored
235
dist/ba_data/python/bastd/activity/multiteamscore.py
vendored
|
|
@ -28,34 +28,43 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
|
|||
super().on_begin()
|
||||
session = self.session
|
||||
if self._show_up_next and isinstance(session, ba.MultiTeamSession):
|
||||
txt = ba.Lstr(value='${A} ${B}',
|
||||
subs=[
|
||||
('${A}',
|
||||
ba.Lstr(resource='upNextText',
|
||||
subs=[
|
||||
('${COUNT}',
|
||||
str(session.get_game_number() + 1))
|
||||
])),
|
||||
('${B}', session.get_next_game_description())
|
||||
])
|
||||
Text(txt,
|
||||
maxwidth=900,
|
||||
h_attach=Text.HAttach.CENTER,
|
||||
v_attach=Text.VAttach.BOTTOM,
|
||||
h_align=Text.HAlign.CENTER,
|
||||
v_align=Text.VAlign.CENTER,
|
||||
position=(0, 53),
|
||||
flash=False,
|
||||
color=(0.3, 0.3, 0.35, 1.0),
|
||||
transition=Text.Transition.FADE_IN,
|
||||
transition_delay=2.0).autoretain()
|
||||
txt = ba.Lstr(
|
||||
value='${A} ${B}',
|
||||
subs=[
|
||||
(
|
||||
'${A}',
|
||||
ba.Lstr(
|
||||
resource='upNextText',
|
||||
subs=[
|
||||
('${COUNT}', str(session.get_game_number() + 1))
|
||||
],
|
||||
),
|
||||
),
|
||||
('${B}', session.get_next_game_description()),
|
||||
],
|
||||
)
|
||||
Text(
|
||||
txt,
|
||||
maxwidth=900,
|
||||
h_attach=Text.HAttach.CENTER,
|
||||
v_attach=Text.VAttach.BOTTOM,
|
||||
h_align=Text.HAlign.CENTER,
|
||||
v_align=Text.VAlign.CENTER,
|
||||
position=(0, 53),
|
||||
flash=False,
|
||||
color=(0.3, 0.3, 0.35, 1.0),
|
||||
transition=Text.Transition.FADE_IN,
|
||||
transition_delay=2.0,
|
||||
).autoretain()
|
||||
|
||||
def show_player_scores(self,
|
||||
delay: float = 2.5,
|
||||
results: ba.GameResults | None = None,
|
||||
scale: float = 1.0,
|
||||
x_offset: float = 0.0,
|
||||
y_offset: float = 0.0) -> None:
|
||||
def show_player_scores(
|
||||
self,
|
||||
delay: float = 2.5,
|
||||
results: ba.GameResults | None = 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
|
||||
|
|
@ -96,7 +105,8 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
|
|||
|
||||
# noinspection PyUnresolvedReferences
|
||||
def _get_player_score_set_entry(
|
||||
player: ba.SessionPlayer) -> ba.PlayerRecord | None:
|
||||
player: ba.SessionPlayer,
|
||||
) -> ba.PlayerRecord | None:
|
||||
for p_rec in valid_players:
|
||||
if p_rec[1].player is player:
|
||||
return p_rec[1]
|
||||
|
|
@ -108,7 +118,8 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
|
|||
for team in winnergroup.teams:
|
||||
if len(team.players) == 1:
|
||||
player_entry = _get_player_score_set_entry(
|
||||
team.players[0])
|
||||
team.players[0]
|
||||
)
|
||||
if player_entry is not None:
|
||||
player_records.append(player_entry)
|
||||
else:
|
||||
|
|
@ -124,33 +135,43 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
|
|||
|
||||
voffs = -140.0 + spacing * len(player_records) * 0.5
|
||||
|
||||
def _txt(xoffs: float,
|
||||
yoffs: float,
|
||||
text: ba.Lstr,
|
||||
h_align: Text.HAlign = Text.HAlign.RIGHT,
|
||||
extrascale: float = 1.0,
|
||||
maxwidth: float | None = 120.0) -> None:
|
||||
Text(text,
|
||||
color=(0.5, 0.5, 0.6, 0.5),
|
||||
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,
|
||||
maxwidth=maxwidth,
|
||||
transition=Text.Transition.IN_LEFT,
|
||||
transition_delay=tdelay).autoretain()
|
||||
def _txt(
|
||||
xoffs: float,
|
||||
yoffs: float,
|
||||
text: ba.Lstr,
|
||||
h_align: Text.HAlign = Text.HAlign.RIGHT,
|
||||
extrascale: float = 1.0,
|
||||
maxwidth: float | None = 120.0,
|
||||
) -> None:
|
||||
Text(
|
||||
text,
|
||||
color=(0.5, 0.5, 0.6, 0.5),
|
||||
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,
|
||||
maxwidth=maxwidth,
|
||||
transition=Text.Transition.IN_LEFT,
|
||||
transition_delay=tdelay,
|
||||
).autoretain()
|
||||
|
||||
session = self.session
|
||||
assert isinstance(session, ba.MultiTeamSession)
|
||||
tval = ba.Lstr(resource='gameLeadersText',
|
||||
subs=[('${COUNT}', str(session.get_game_number()))])
|
||||
_txt(180,
|
||||
43,
|
||||
tval,
|
||||
h_align=Text.HAlign.CENTER,
|
||||
extrascale=1.4,
|
||||
maxwidth=None)
|
||||
tval = ba.Lstr(
|
||||
resource='gameLeadersText',
|
||||
subs=[('${COUNT}', str(session.get_game_number()))],
|
||||
)
|
||||
_txt(
|
||||
180,
|
||||
43,
|
||||
tval,
|
||||
h_align=Text.HAlign.CENTER,
|
||||
extrascale=1.4,
|
||||
maxwidth=None,
|
||||
)
|
||||
_txt(-15, 4, ba.Lstr(resource='playerText'), h_align=Text.HAlign.LEFT)
|
||||
_txt(180, 4, ba.Lstr(resource='killsText'))
|
||||
_txt(280, 4, ba.Lstr(resource='deathsText'), maxwidth=100)
|
||||
|
|
@ -162,52 +183,80 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
|
|||
|
||||
topkillcount = 0
|
||||
topkilledcount = 99999
|
||||
top_score = 0 if not player_records else _get_prec_score(
|
||||
player_records[0])
|
||||
top_score = (
|
||||
0 if not player_records else _get_prec_score(player_records[0])
|
||||
)
|
||||
|
||||
for prec in player_records:
|
||||
topkillcount = max(topkillcount, prec.accum_kill_count)
|
||||
topkilledcount = min(topkilledcount, prec.accum_killed_count)
|
||||
|
||||
def _scoretxt(text: str | ba.Lstr,
|
||||
x_offs: float,
|
||||
highlight: bool,
|
||||
delay2: float,
|
||||
maxwidth: float = 70.0) -> None:
|
||||
Text(text,
|
||||
position=(ts_h_offs + x_offs * 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),
|
||||
h_align=Text.HAlign.RIGHT,
|
||||
v_align=Text.VAlign.CENTER,
|
||||
maxwidth=maxwidth,
|
||||
transition=Text.Transition.IN_LEFT,
|
||||
transition_delay=tdelay + delay2).autoretain()
|
||||
def _scoretxt(
|
||||
text: str | ba.Lstr,
|
||||
x_offs: float,
|
||||
highlight: bool,
|
||||
delay2: float,
|
||||
maxwidth: float = 70.0,
|
||||
) -> None:
|
||||
Text(
|
||||
text,
|
||||
position=(
|
||||
ts_h_offs + x_offs * 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),
|
||||
h_align=Text.HAlign.RIGHT,
|
||||
v_align=Text.VAlign.CENTER,
|
||||
maxwidth=maxwidth,
|
||||
transition=Text.Transition.IN_LEFT,
|
||||
transition_delay=tdelay + delay2,
|
||||
).autoretain()
|
||||
|
||||
for playerrec in player_records:
|
||||
tdelay += 0.05
|
||||
voffs -= spacing
|
||||
Image(playerrec.get_icon(),
|
||||
position=(ts_h_offs - 12 * 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.getname(full=True)),
|
||||
maxwidth=160,
|
||||
scale=0.75 * scale,
|
||||
position=(ts_h_offs + 10.0 * scale,
|
||||
ts_v_offset + (voffs + 15) * scale),
|
||||
h_align=Text.HAlign.LEFT,
|
||||
v_align=Text.VAlign.CENTER,
|
||||
color=ba.safecolor(playerrec.team.color + (1, )),
|
||||
transition=Text.Transition.IN_LEFT,
|
||||
transition_delay=tdelay).autoretain()
|
||||
_scoretxt(str(playerrec.accum_kill_count), 180,
|
||||
playerrec.accum_kill_count == topkillcount, 0.1)
|
||||
_scoretxt(str(playerrec.accum_killed_count), 280,
|
||||
playerrec.accum_killed_count == topkilledcount, 0.1)
|
||||
_scoretxt(_get_prec_score_str(playerrec), 390,
|
||||
_get_prec_score(playerrec) == top_score, 0.2)
|
||||
Image(
|
||||
playerrec.get_icon(),
|
||||
position=(
|
||||
ts_h_offs - 12 * 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.getname(full=True)),
|
||||
maxwidth=160,
|
||||
scale=0.75 * scale,
|
||||
position=(
|
||||
ts_h_offs + 10.0 * scale,
|
||||
ts_v_offset + (voffs + 15) * scale,
|
||||
),
|
||||
h_align=Text.HAlign.LEFT,
|
||||
v_align=Text.VAlign.CENTER,
|
||||
color=ba.safecolor(playerrec.team.color + (1,)),
|
||||
transition=Text.Transition.IN_LEFT,
|
||||
transition_delay=tdelay,
|
||||
).autoretain()
|
||||
_scoretxt(
|
||||
str(playerrec.accum_kill_count),
|
||||
180,
|
||||
playerrec.accum_kill_count == topkillcount,
|
||||
0.1,
|
||||
)
|
||||
_scoretxt(
|
||||
str(playerrec.accum_killed_count),
|
||||
280,
|
||||
playerrec.accum_killed_count == topkilledcount,
|
||||
0.1,
|
||||
)
|
||||
_scoretxt(
|
||||
_get_prec_score_str(playerrec),
|
||||
390,
|
||||
_get_prec_score(playerrec) == top_score,
|
||||
0.2,
|
||||
)
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue