Merge branch '1.7' into public-server

This commit is contained in:
Ayush Saini 2022-11-06 01:05:32 +05:30
commit b29dbb2f2b
283 changed files with 35136 additions and 23221 deletions

View file

@ -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()

View file

@ -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')

View file

@ -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꧂",

View file

@ -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": "帮助",

View file

@ -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": "幫助",

View file

@ -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",

View file

@ -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.",

View file

@ -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",

View file

@ -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",

View file

@ -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"

View file

@ -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",

View file

@ -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": "Πως Παίζεται;",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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": "Как играть",

View file

@ -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",

View file

@ -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": "எப்படி விளையாடுவது",

View file

@ -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",

View file

@ -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": "Як грати",

View file

@ -13,6 +13,7 @@
"linkAccountsInstructionsNewText": "Par cołegar do account, jènara un còdaze inte'l primo\ndispozidivo e insarìseło intel segondo. I dati del\nsegondo account i vegnarà sparpagnài so tuti do i account\n(i dati del 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 intel mèdemo momento.",
"linkAccountsText": "Cołega account",
"linkedAccountsText": "Account cołegài:",
"manageAccountText": "Jestisi account",
"nameChangeConfirm": "Vutu canbiar el nome del 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).\nSta 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",

View file

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

View file

@ -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()

View file

@ -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()

View file

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

File diff suppressed because it is too large Load diff

View file

@ -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:

View file

@ -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,
)

View file

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

View file

@ -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()

View file

@ -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/', '')

View file

@ -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()

View file

@ -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"
)

View file

@ -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
)

View file

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

View file

@ -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):

View file

@ -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
)

View file

@ -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)

View file

@ -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',
),
],
))
)
)

View file

@ -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()

View file

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

View file

@ -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()

View file

@ -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:

View file

@ -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}
)
)

View file

@ -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:

View file

@ -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},
)
)

File diff suppressed because it is too large Load diff

View file

@ -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] = []

View file

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

View file

@ -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:

View file

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

View file

@ -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())

View file

@ -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()

View file

@ -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()

View file

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

View file

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

View file

@ -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:

View file

@ -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()

View file

@ -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, ...]:

View file

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

View file

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

View file

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

View file

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

View file

@ -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()

View file

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

View file

@ -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'
}]
]

View file

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

View file

@ -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),
)

View file

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

View file

@ -18,6 +18,7 @@ class ScoreType(Enum):
Category: **Enums**
"""
SECONDS = 's'
MILLISECONDS = 'ms'
POINTS = 'p'

View file

@ -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)

View file

@ -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:

View file

@ -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]]

View file

@ -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')

View file

@ -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()

View file

@ -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()

View file

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

View file

@ -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 += [

View file

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

View file

@ -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()

View file

@ -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)

View file

@ -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',

View file

@ -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)))
)

View file

@ -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.')

View file

@ -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,
)

View file

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

View file

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

View file

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

View file

@ -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
)

View file

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

View file

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

View file

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

View file

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

View file

@ -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()

File diff suppressed because it is too large Load diff

View file

@ -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))

View file

@ -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()

View file

@ -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)

View file

@ -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()

View file

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