diff --git a/README.md b/README.md index ba5bad3..e0c2ff7 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # Bombsquad-Ballistica-Modded-Server -Modder server scripts to host ballistica (Bombsquad).Running on BS1.7.2. +Modder server scripts to host ballistica (Bombsquad).Running on BS1.7.10. ## Requirements -- Ubuntu 20 +- Ubuntu 20 and above - python3.10 ## Getting Started @@ -33,6 +33,7 @@ Here you can ban player , mute them , disable their kick votes ## Features - Rank System. - Chat commands. +- V2 Account with cloud console for server. - Easy role management , create 1000 of roles as you wish add specific chat command to the role , give tag to role ..many more. - Rejoin cooldown. - Leaderboard , top 3 rank players name on top right corner. @@ -62,6 +63,9 @@ Here you can ban player , mute them , disable their kick votes - Integrated ElPatronPowerups. - Auto switch to coop mode when players are less then threshold. - Change playlist on fly with playlist code or name , i.e /playlist teams , /playlist coop , /playlist 34532 +- rotate prop nodes with node.changerotation(x,y,z) +- set 2d mode with _ba.set_2d_mode(true) +- set 2d plane with _ba.set_2d_plane(z) - beta , not works with spaz.fly = true. - New Splitted Team in game score screen. - New final score screen , StumbledScoreScreen. - other small small feature improvement here there find yourself. diff --git a/ballisticacore_server b/ballisticacore_server new file mode 100644 index 0000000..dad8792 --- /dev/null +++ b/ballisticacore_server @@ -0,0 +1,901 @@ +#!/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() diff --git a/dist/ba_data/data/langdata.json b/dist/ba_data/data/langdata.json index 088d16a..718dbc6 100644 --- a/dist/ba_data/data/langdata.json +++ b/dist/ba_data/data/langdata.json @@ -70,9 +70,11 @@ "Abinav", "Abir", "Abolfadl", + "Abolfazl", "Abraham", "Roman Abramov", "AC", + "Achref", "adan", "Adeel (AdeZ {@adez_})", "Adel", @@ -287,7 +289,9 @@ "bob bobber", "The Bomboler 💣", "bombsquad", + "Bombsquadzueira", "Bomby", + "Zeleni bomby", "Book", "Lucas Borges", "Gianfranco Del Borrello", @@ -337,6 +341,7 @@ "Fabio Cannavacciuolo", "CANOVER", "Fedrigo Canpanjoło", + "CarlosE.", "mark Dave a carposo", "Fabricio de Carvalho", "Joshua Castañeda", @@ -370,6 +375,7 @@ "David Cot", "Nayib Méndez Coto", "Dylan cotten", + "covcheg", "COVER", "crac", "CrazyBear", @@ -424,6 +430,7 @@ "Diase7en", "ferbie Dicen", "Diego788", + "DiegoGD", "DiGGDaGG", "dikivan2000", "Dimitriy", @@ -437,6 +444,8 @@ "dlw", "DMarci", "Dmirus", + "Dmitriy", + "Savchenko Dmitriy", "Count Su Doku", "DominikSikora!", "Kai Dominique", @@ -482,6 +491,7 @@ "EnderDust123", "EnderKay", "EnglandFirst", + "Enrico", "enzo", "Erick", "Erkam", @@ -511,6 +521,7 @@ "Syed Irfan Farhan", "Luiz Henrique Faria", "Syed Fahrin Farihin", + "Fatih", "FaultyAdventure", "Putra Riski Fauzi", "fauziozan.23@gmail.com", @@ -558,6 +569,7 @@ "Gabriele", "Nihar Gajare", "GalaxyNinja2003", + "AP - Pro Gamer", "Proff Gamer", "Eduardo Gamer05", "Taufiq Gamera", @@ -583,6 +595,7 @@ "Giovalli99", "Giovanny", "Dc superhero girl", + "Givij", "Glu10free", "Mr. Glu10free", "Jhon Zion N. delos reyes gmail", @@ -694,10 +707,11 @@ "Anestis Ioakimidis", "Dragomir Ioan", "Isa", + "Israelme03", "Tobias Dencker Israelsen", "Kegyes István", "Itamar", - "Ivan", + "ivan", "iViietZ", "JaaJ", "Al jabbar", @@ -752,6 +766,7 @@ "Jules", "juse", "Justine", + "JYLE", "Jyothish", "Oliver Jõgar", "Nackter Jörg", @@ -866,6 +881,7 @@ "Linux44313", "LiteBalt", "LittleNyanCat", + "Juunhao Liu", "Lizz", "Lizzetc", "Lkham", @@ -879,6 +895,7 @@ "lorenzo", "Lostguybrazil", "mian louw", + "69 lover", "Jordan Vega Loza", "Chenging Lu", "Chengming Lu", @@ -889,6 +906,7 @@ "Ludovico", "Luis (GalaxyM4)", "Jose Luis", + "Luis(GalaxtM4)", "luislinares", "luispro25", "Luka", @@ -948,6 +966,8 @@ "Matteo", "Matthias", "Ihsan Maulana ( @ihsanm27)", + "Muhammad Akbar Maulana", + "Mavook", "Federico Mazzone", "Andrea Mazzucchelli", "Medic", @@ -982,6 +1002,7 @@ "Mk", "MKG", "mobin", + "Mobina", "Moh", "Mohamadali", "Mohamadamin", @@ -1011,6 +1032,7 @@ "MrS0meone", "Ivan Ms", "Msta", + "MT", "Muhammed Muhsin", "MujtabaFR", "Muni", @@ -1060,6 +1082,7 @@ "طارق محمد رضا سعيد NinjaStarXD", "nino", "Nintendero65", + "Nizril", "Nnubes256", "Bu nny", "Noam", @@ -1067,6 +1090,7 @@ "NofaseCZ", "Max Noisa", "Noisb", + "None", "Noobslaya101", "noorjandle1", "Petter Nordlander", @@ -1076,10 +1100,12 @@ "Dhimas Wildan Nz", "*** Adel NZ. ***", "Ognjen", + "okko", "Bastián Olea", "Nikita Oleshko", "Omar", "On3GaMs", + "No one", "Adam Oros", "Andrés Ortega", "Zangar Orynbetov", @@ -1091,6 +1117,7 @@ "Giorgio Palmieri", "Abhinay Pandey", "PangpondTH", + "PanKonKezo", "PantheRoP", "ParadoxPlayz", "Gavin Park", @@ -1109,9 +1136,11 @@ "pc192089", "PC261133", "PC295933", + "PC432736", "pebikristia", "Pedro", "Jiren/Juan Pedro", + "Penta :D", "Peque", "Rode Liliana Miranda Pereira", "Jura Perić", @@ -1147,6 +1176,7 @@ "Pong", "Pooya", "pouriya", + "Pouya", "Pranav", "Luca Preibsch", "Prem", @@ -1172,6 +1202,7 @@ "raghul", "khaled rahma", "Rayhan Rahmats", + "Ralfreengz", "1. Ramagister", "Rostislav RAMAGISTER", "Ростислав RAMAGISTER", @@ -1276,6 +1307,7 @@ "Jhon Rodel Sayo", "Christiaan Schriel", "Hendrik Schur", + "SEBASTIAN2059", "Semen", "Mihai Serbanica", "Daniel Balam Cabrera Serrano", @@ -1312,6 +1344,7 @@ "sobhan", "Nikhil sohan", "SoK", + "SoldierBS", "SPT Sosat", "Soto", "SpacingBat3", @@ -1377,12 +1410,14 @@ "TempVolcano3200", "Yan Teryokhin", "TestGame1", + "TestGame1👽🔥", "testwindows8189", "tgd4", "Than", "Thanakorn7215", "thatFlaviooo", "The_Blinded", + "Eugene (a.k.a TheBomber3000)", "Thebosslol66", "thejoker190101", "TheLLage", @@ -1426,6 +1461,7 @@ "Uros", "clarins usap", "Uzinerz", + "Shohrux V", "Vader", "Valentin", "Valkan1975", @@ -1463,6 +1499,7 @@ "webparham", "Wesley", "whitipet", + "wibi9424", "Wido2000", "wildanae", "Will", @@ -1482,6 +1519,7 @@ "Francisco Xavier", "xbarix123897", "Peque XD", + "Xem", "Xizruh", "xxonx8", "Ajeet yadav", @@ -1496,6 +1534,7 @@ "Kenneth Yoneyama", "yossef", "youcef", + "Youssef", "Yousuf", "Yovan182Sunbreaker", "Yrtking", @@ -1503,6 +1542,7 @@ "Yudhis", "yugo", "yullian", + "Yuslendo", "NEEROOA Muhammad Yusuf", "Yuuki", "Yy", @@ -1527,6 +1567,7 @@ "Riven Zhao", "jim ZHOU", "Mohammad ziar", + "ZioFesteeeer", "zJairO", "ZkyweR", "Nagy Zoltán", @@ -1599,6 +1640,8 @@ "محمد خالد", "امیرحسین دهقان", "امید رضازاده", + "فاطمه عباس زاده ۸۴", + "فاطمه عباس زاده۸۴", "محمد وائل سلطان", "ص", "عبداللہ صائم", @@ -1657,9 +1700,11 @@ "神仙", "药药Medic", "蔚蓝枫叶", + "陈星宇你就是歌姬吧", "鲨鱼服·Medic", "鲲鹏元帅", "꧁ℤephyro꧂", + "가라사대", "공팔이", "권찬근", "김원재", @@ -1673,6 +1718,8 @@ "이지민", "일베저장소", "전감호", - "BombsquadKorea 네이버 카페" + "BombsquadKorea 네이버 카페", + "Zona-BombSquad", + "CrazySquad" ] } diff --git a/dist/ba_data/data/languages/arabic.json b/dist/ba_data/data/languages/arabic.json index f0c7a36..26aa890 100644 --- a/dist/ba_data/data/languages/arabic.json +++ b/dist/ba_data/data/languages/arabic.json @@ -499,6 +499,7 @@ "welcome2Text": "يمكنك أيضا الحصول على تذاكر من العديد من الأنشطة نفسها.\nتذاكر يمكن استخدامها لفتح شخصيات جديدة، والخرائط، و\nالألعاب المصغرة، للدخول البطولات، وأكثر من ذلك.", "yourPowerRankingText": "تصنيف الطاقة:" }, + "copyConfirmText": "نسخ إلى اللوحة", "copyOfText": "${NAME} نسخ", "copyText": "ينسخ", "createEditPlayerText": "<اصنع او عدل حساب>", @@ -626,7 +627,7 @@ "epicDescriptionFilterText": "${DESCRIPTION} بحركة ملحمية بطيئة", "epicNameFilterText": "الملحمي ${NAME}", "errorAccessDeniedText": "تم الرفض", - "errorDeviceTimeIncorrectText": "توقف وقت جهازك بمقدار ${HOURS} ساعة.\nهذا قد يتسبب بمشاكل.\nمن فضلك قم بالتحقق من اعدادات الوقت.", + "errorDeviceTimeIncorrectText": ".من الساعات ${HOURS} وقت جهازك غير صحيح بمقدار\n.هذا سوف يتسبب بمشاكل\nمن فضلك قم بالتحقق من اعدادات الوقت.", "errorOutOfDiskSpaceText": "انتهت مساحة التخزين", "errorSecureConnectionFailText": "تعذر انشاء اتصال سحابي أمن; قد تفشل وظائف الشبكة.", "errorText": "خطا", @@ -1367,6 +1368,7 @@ "tournamentStandingsText": "ترتيب البطولة", "tournamentText": "المسابقة", "tournamentTimeExpiredText": "انتهت مدة البطولة", + "tournamentsDisabledWorkspaceText": "البطولات لا تعمل عندما تكون فضائات العمل تعمل.\nلتشغيل البطولات مجددا، قم بإلغاء تشغيل فضاء العمل الخاص بك و اعادة تشغيل اللعبة.", "tournamentsText": "البطولات", "translations": { "characterNames": { diff --git a/dist/ba_data/data/languages/chinese.json b/dist/ba_data/data/languages/chinese.json index 45ef14a..3530828 100644 --- a/dist/ba_data/data/languages/chinese.json +++ b/dist/ba_data/data/languages/chinese.json @@ -501,6 +501,7 @@ "welcome2Text": "你还可参加很多相同活动来赢取点券。\n点券可用于解锁新的角色、地图和\n迷你游戏,或进入锦标赛,或更多用途", "yourPowerRankingText": "你的能力排位:" }, + "copyConfirmText": "复制到剪贴板", "copyOfText": "${NAME} 复制", "copyText": "copy", "createEditPlayerText": "<创建/编辑玩家>", @@ -630,7 +631,7 @@ "epicDescriptionFilterText": "史诗级慢动作 ${DESCRIPTION}。", "epicNameFilterText": "史诗级${NAME}", "errorAccessDeniedText": "访问被拒绝", - "errorDeviceTimeIncorrectText": "您的系统时间与服务器时间相差了${HOURS}小时\n这可能会出现一些问题\n请检查您的系统时间或时区", + "errorDeviceTimeIncorrectText": "您设备的时间有 ${HOURS} 小时的误差。\n这会导致游戏出现问题。\n请检查您设备的时间和时区设置。", "errorOutOfDiskSpaceText": "磁盘空间不足", "errorSecureConnectionFailText": "无法建立安全的云链接,网络可能会连接失败", "errorText": "错误", @@ -1385,6 +1386,7 @@ "tournamentStandingsText": "锦标赛积分榜", "tournamentText": "锦标赛", "tournamentTimeExpiredText": "锦标赛时间结束", + "tournamentsDisabledWorkspaceText": "工作区启用时无法参加锦标赛!\n关闭工作区,才能进入锦标赛。", "tournamentsText": "锦标赛", "translations": { "characterNames": { diff --git a/dist/ba_data/data/languages/chinesetraditional.json b/dist/ba_data/data/languages/chinesetraditional.json index 6f0f25f..c7c1a69 100644 --- a/dist/ba_data/data/languages/chinesetraditional.json +++ b/dist/ba_data/data/languages/chinesetraditional.json @@ -787,7 +787,7 @@ "ticketPack4Text": "局型點券包", "ticketPack5Text": "巨巨巨巨巨巨巨巨巨巨型點券包", "ticketPack6Text": "終極點券包", - "ticketsFromASponsorText": "從贊助商\n獲取${COUNT}點券", + "ticketsFromASponsorText": "看推廣影片\n獲取${COUNT}點券", "ticketsText": "${COUNT} 點券", "titleText": "獲得點券", "unavailableLinkAccountText": "對不起,該平台不可進行購買\n您可以將賬戶鏈接到另一個\n平台,以進行購買", @@ -798,6 +798,7 @@ "youHaveText": "你擁有 ${COUNT}點券" }, "googleMultiplayerDiscontinuedText": "抱歉,Google的多人遊戲服務不再可用。\n我將盡快更換新的替代服務。\n在此之前,請嘗試其他連接方法。\n-Eric", + "googlePlayPurchasesNotAvailableText": "Google Play購買不可用\n你可能需要更新你的Google Play商店組件", "googlePlayText": "Google Play", "graphicsSettingsWindow": { "alwaysText": "總是", @@ -1362,6 +1363,7 @@ "tournamentStandingsText": "錦標賽積分榜", "tournamentText": "錦標賽", "tournamentTimeExpiredText": "錦標賽時間結束", + "tournamentsDisabledWorkspaceText": "儅工作區處於開啓狀態時講標賽將被禁用\n如果想解禁錦標賽,請關閉您的工作區並重啓游戲", "tournamentsText": "錦標賽", "translations": { "characterNames": { diff --git a/dist/ba_data/data/languages/croatian.json b/dist/ba_data/data/languages/croatian.json index f352d1c..e88c2e1 100644 --- a/dist/ba_data/data/languages/croatian.json +++ b/dist/ba_data/data/languages/croatian.json @@ -1,10 +1,10 @@ { "accountSettingsWindow": { - "accountNameRules": "Ime računa nemože satržavati emotikone ili ostale posebne znakove", + "accountNameRules": "Korisničko ime ne može sadržavati emotikone ili druge posebne znakove", "accountProfileText": "(korisnički račun)", - "accountsText": "Profili", + "accountsText": "Korisnički računi", "achievementProgressText": "Postignuća: ${COUNT} od ${TOTAL}", - "campaignProgressText": "Napredak u kampanji [Teško]: ${PROGRESS}", + "campaignProgressText": "Napredak kampanje[Teško]: ${PROGRESS}", "changeOncePerSeason": "Ovo možeš promjeniti samo jednom po sezoni.", "changeOncePerSeasonError": "Moraš pričekati sljedeču sezonu da promjeniš ovo(${NUM} days)", "customName": "Prilagođeno ime", @@ -14,7 +14,7 @@ "linkAccountsInstructionsNewText": "Za povezivanje dva računa, generiraj kod sa prvog\ni unesi taj kod na drugi. Podaci s drugog računa\nće biti podjeljeni između oba.\n(Podaci s prvog računa će biti izgubljeni)\n\nMožeš povezati do ${COUNT} računa.\n\nVAŽNO: povezuj samo vlastite račune;\nAko povežeš prijateljev račun onda nećete moći\nzajedno igrati u isto vrijeme.", "linkAccountsInstructionsText": "Da povežeš dva profila, stvori kod na \njednomod njih i unesi ga na drugom.\nNapredak i sve kupljeno bit će kombinirano.\nMožeš povezati najviše ${COUNT} profila.", "linkAccountsText": "Poveži profile", - "linkedAccountsText": "Povezani profili:", + "linkedAccountsText": "Povezani računi:", "nameChangeConfirm": "Promjeni svoje ime u ${NAME}?", "resetProgressConfirmNoAchievementsText": "Ovo će poništiti tvoj napredak u timskom modu i\ntvoje najbolje rezultate (ali ne i tvoje kupone).\nNemaš mogućnost povratka. Jesi li siguran?", "resetProgressConfirmText": "Ovo će poništiti tvoj napredak u timskom modu,\npostignuća, i vaše najbolje rezultate\n(ali ne i tvoje kupone). Nemaš mogućnost\npovratka. Jesi li siguran?", diff --git a/dist/ba_data/data/languages/czech.json b/dist/ba_data/data/languages/czech.json index f130007..775db13 100644 --- a/dist/ba_data/data/languages/czech.json +++ b/dist/ba_data/data/languages/czech.json @@ -801,7 +801,7 @@ "ticketPack4Text": "Sloní Balíček Kupónů", "ticketPack5Text": "Mamutí Balíček Kupónů!", "ticketPack6Text": "Ultimátní Balíček Kupónů", - "ticketsFromASponsorText": "Získat ${COUNT} kupónů\nod sponzora", + "ticketsFromASponsorText": "Zhlédni reklamu \nza ${COUNT} tiketů", "ticketsText": "${COUNT} Kupónů", "titleText": "Získat Kupóny", "unavailableLinkAccountText": "Omlouváme se, ale nákupy nejsou na této platformě možné.\nJako řešení je, že můžete si tento účet propojit s jiným\nna jiné platformě a uskutečnit nákup tam.", @@ -812,6 +812,7 @@ "youHaveText": "Máte ${COUNT} kupónů" }, "googleMultiplayerDiscontinuedText": "Litujeme, služba pro více hráčů Google již není k dispozici.\n Pracuji na výměně co nejrychleji.\n Do té doby zkuste jiný způsob připojení.\n -Eric", + "googlePlayPurchasesNotAvailableText": "Nákupy na Google Play nejsou k dispozici.\nMožná budete muset aktualizovat obchod play.", "googlePlayText": "Google Play", "graphicsSettingsWindow": { "alwaysText": "Vždy", @@ -1385,6 +1386,7 @@ "tournamentStandingsText": "Pořadí v turnaji", "tournamentText": "Turnaj", "tournamentTimeExpiredText": "Čas turnaje vypršel", + "tournamentsDisabledWorkspaceText": "Turnaje jsou zakázány, když jsou aktivní pracovní prostory.\nChcete-li znovu povolit turnaje, deaktivujte svůj pracovní prostor a restartujte.", "tournamentsText": "Turnaje", "translations": { "characterNames": { diff --git a/dist/ba_data/data/languages/english.json b/dist/ba_data/data/languages/english.json index 528aad5..e35d9ba 100644 --- a/dist/ba_data/data/languages/english.json +++ b/dist/ba_data/data/languages/english.json @@ -497,6 +497,7 @@ "welcome2Text": "You can also earn tickets from many of the same activities.\nTickets can be used to unlock new characters, maps, and\nmini-games, to enter tournaments, and more.", "yourPowerRankingText": "Your Power Ranking:" }, + "copyConfirmText": "Copied to clipboard.", "copyOfText": "${NAME} Copy", "copyText": "Copy", "createEditPlayerText": "", @@ -624,7 +625,7 @@ "epicDescriptionFilterText": "${DESCRIPTION} In epic slow motion.", "epicNameFilterText": "Epic ${NAME}", "errorAccessDeniedText": "access denied", - "errorDeviceTimeIncorrectText": "Your device's time is off by ${HOURS} hours.\nThis is likely to cause problems.\nPlease check your time and time-zone settings.", + "errorDeviceTimeIncorrectText": "Your device's time is incorrect by ${HOURS} hours.\nThis is likely to cause problems.\nPlease check your time and time-zone settings.", "errorOutOfDiskSpaceText": "out of disk space", "errorSecureConnectionFailText": "Unable to establish secure cloud connection; network functionality may fail.", "errorText": "Error", @@ -1074,7 +1075,6 @@ "otherText": "Other...", "outOfText": "(#${RANK} out of ${ALL})", "ownFlagAtYourBaseWarning": "Your own flag must be\nat your base to score!", - "packageModsEnabledErrorText": "Network-play is not allowed while local-package-mods are enabled (see Settings->Advanced)", "partyWindow": { "chatMessageText": "Chat Message", "emptyText": "Your party is empty", @@ -1245,8 +1245,6 @@ "disableCameraGyroscopeMotionText": "Disable Camera Gyroscope Motion", "disableCameraShakeText": "Disable Camera Shake", "disableThisNotice": "(you can disable this notice in advanced settings)", - "enablePackageModsDescriptionText": "(enables extra modding capabilities but disables net-play)", - "enablePackageModsText": "Enable Local Package Mods", "enterPromoCodeText": "Enter Code", "forTestingText": "Note: these values are only for testing and will be lost when the app exits.", "helpTranslateText": "${APP_NAME}'s non-English translations are a community\nsupported effort. If you'd like to contribute or correct\na translation, follow the link below. Thanks in advance!", @@ -1375,6 +1373,7 @@ "tournamentStandingsText": "Tournament Standings", "tournamentText": "Tournament", "tournamentTimeExpiredText": "Tournament Time Expired", + "tournamentsDisabledWorkspaceText": "Tournaments are disabled when workspaces are active.\nTo re-enable tournaments, disable your workspace and restart.", "tournamentsText": "Tournaments", "translations": { "characterNames": { diff --git a/dist/ba_data/data/languages/filipino.json b/dist/ba_data/data/languages/filipino.json index a208c03..e98e35f 100644 --- a/dist/ba_data/data/languages/filipino.json +++ b/dist/ba_data/data/languages/filipino.json @@ -176,7 +176,7 @@ "descriptionComplete": "Nanalo ng walang puntos ang kalaban", "descriptionFull": "Manalo sa ${LEVEL} ng walang puntos ang kalaban", "descriptionFullComplete": "Nanalo sa ${LEVEL} ng walang puntos ang kalaban", - "name": "Buwaya sa ${LEVEL}" + "name": "Pagsarhan ng ${LEVEL}" }, "Pro Football Victory": { "description": "Panalunin ang laro", @@ -204,7 +204,7 @@ "descriptionComplete": "Nanalo nang hindi hinahayaang makapuntos ang kalaban", "descriptionFull": "Manalo sa ${LEVEL} nang hindi hinahayaang makapuntos ang kalaban", "descriptionFullComplete": "Nanalo sa ${LEVEL} nang hindi hinahayaang makapuntos ang kalaban", - "name": "${LEVEL} Shutout" + "name": "Pagsarhan ng ${LEVEL}" }, "Rookie Football Victory": { "description": "Ipanalo ang laro", @@ -232,14 +232,14 @@ "descriptionComplete": "Nakapuntos ng 500", "descriptionFull": "Pumuntos ng 500 sa ${LEVEL}", "descriptionFullComplete": "Nakakuha ng 500 puntos sa ${LEVEL}", - "name": "${LEVEL} Master" + "name": "Pinuno ng ${LEVEL}" }, "Runaround Wizard": { "description": "Pumuntos ng 1000", "descriptionComplete": "Naka score ng 1000 points", "descriptionFull": "Mag score ng 1000 points sa ${LEVEL}", "descriptionFullComplete": "Naka score ng 1000 points sa ${LEVEL}", - "name": "${LEVEL} Wizard" + "name": "Salamangkero ng ${LEVEL}" }, "Sharing is Caring": { "descriptionFull": "I-share ang game sa iyong kaibigan", @@ -247,10 +247,10 @@ "name": "Ang pagbigay ay pag-alaga" }, "Stayin' Alive": { - "description": "Manalo nang hindi namamatay", + "description": "Manalo nang hindi namatay", "descriptionComplete": "Nanalo nang hindi namatay", "descriptionFull": "Nanalo ${LEVEL} nang hindi namatay", - "descriptionFullComplete": "Nanalo ${LEVEL} nang hindi namatay", + "descriptionFullComplete": "Nanalo sa ${LEVEL} nang hindi namatay", "name": "Manatiling Buhay" }, "Super Mega Punch": { @@ -298,7 +298,7 @@ "descriptionComplete": "Manalo nang hindi maka puntos ang mga kalaban", "descriptionFull": "Manalo sa ${LEVEL} na hindi maka puntos ang mga kalaban", "descriptionFullComplete": "Manalo sa ${LEVEL} na hindi maka puntos ang mga kalaban", - "name": "${LEVEL} Pagsarhan" + "name": "Pagsarhan ng ${LEVEL}" }, "Uber Football Victory": { "description": "Ipanalo ang laro", @@ -357,10 +357,10 @@ "buttonText": "pindutan", "canWeDebugText": "Gusto mo ba na ang BombSquad ay automatic na mag report ng\nbugs, crashes, at mga basic usage na info na i-sent sa developer?\n\nHindi ito naglalaman ng mga personal information at makatulong ito\npara ang laro ay gumagana at bug-free.", "cancelText": "Kanselahin", - "cantConfigureDeviceText": "Pasensya na, ang ${DEVICE} ay hindi ma-configure.", + "cantConfigureDeviceText": "Pasensya na, ang ${DEVICE} na ito ay hindi ma-configure.", "challengeEndedText": "Natapos na ang challenge na ito.", "chatMuteText": "I-mute ang Chat", - "chatMutedText": "Chat Muted", + "chatMutedText": "Na-mute ang Chat", "chatUnMuteText": "I-unmute ang Chat", "choosingPlayerText": "", "completeThisLevelToProceedText": "I complete mo muna\nang level na ito bago ka mag-proceed!", @@ -497,6 +497,7 @@ "welcome2Text": "Maaari ka ring makakuha ng mga tiket mula sa marami sa parehong mga aktibidad.\nMaaaring gamitin ang mga tiket para i-unlock ang mga bagong character, mapa, at\nmini-games, para makapasok sa mga tournament, at higit pa.", "yourPowerRankingText": "Iyong Power Ranking:" }, + "copyConfirmText": "Nakopya sa clipboard.", "copyOfText": "Kopya ng ${NAME}", "copyText": "I-kopya", "createEditPlayerText": "", @@ -516,7 +517,7 @@ "specialThanksText": "Espesyal Na Pasasalamat:", "thanksEspeciallyToText": "Salamat, lalo na kay ${NAME}", "titleText": "${APP_NAME} Mga Kredito", - "whoeverInventedCoffeeText": "Kung sino man nag-imbento ng kape" + "whoeverInventedCoffeeText": "At ang sino man na nag-imbento ng kape" }, "currentStandingText": "Ang kasalukuyang tayo mo ay #${RANK}", "customizeText": "I-customize...", @@ -624,7 +625,7 @@ "epicDescriptionFilterText": "${DESCRIPTION} sa isang epic na slow motion.", "epicNameFilterText": "Epikong ${NAME}", "errorAccessDeniedText": "walang pahintulot", - "errorDeviceTimeIncorrectText": "Ang oras ng iyong device ay naka-off nang ${HOURS} na oras.\nIto ay malamang na magdulot ng mga iba’t ibang problema.\nPakisuri ang iyong mga setting ng oras at time-zone.", + "errorDeviceTimeIncorrectText": "Ang oras ng iyong device ay di tama nang ${HOURS} na oras.\nIto ay malamang na magdulot ng mga iba’t ibang problema.\nPakisuri ang mga setting ng iyong oras at time-zone.", "errorOutOfDiskSpaceText": "Wala sa puwang ng disk", "errorSecureConnectionFailText": "Hindi mapatunayan ng secure na “cloud connection”; maaaring mabigo ang pagpapagana ng network.", "errorText": "Error", @@ -870,12 +871,12 @@ "hostIsNavigatingMenusText": "- Ang ${HOST} ay nagna-navigate sa mga menu tulad ng isang boss -", "importPlaylistCodeInstructionsText": "Gamitin ang sumusunod na code upang i-import ang playlist na ito sa ibang lugar:", "importPlaylistSuccessText": "Na-import na ${TYPE} na playlist '${NAME}'", - "importText": "Iangkat", - "importingText": "Pag-Import…", - "inGameClippedNameText": "in-game ay naging\n\"${NAME}\"", + "importText": "I-Import", + "importingText": "Nag-Iimport…", + "inGameClippedNameText": "Sa in-game ay naging\n\"${NAME}\"", "installDiskSpaceErrorText": "ERROR: Hindi makumpleto ang pag-install.\nMaaaring wala ka nang espasyo sa iyong device.\nMag-clear ng ilang espasyo at subukang muli.", "internal": { - "arrowsToExitListText": "pindutin ang ${LEFT} o ${RIGHT} upang lumabas sa listahan", + "arrowsToExitListText": "pindutin ang ${LEFT} o ${RIGHT} upang mawala sa listahan", "buttonText": "pindutan", "cantKickHostError": "Hindi mo maaaring I-kick ang host.", "chatBlockedText": "Na-block si ${NAME} sa loob ng ${TIME} segundo.", @@ -922,7 +923,7 @@ "serverRestartingText": "Nagre-restart ang server. Mangyaring sumali muli sa isang saglit…", "serverShuttingDownText": "Nagsasara ang server...", "signInErrorText": "Error sa pag-sign in.", - "signInNoConnectionText": "Hindi makapag-sign in. (walang koneksyon sa internet?)", + "signInNoConnectionText": "Hindi makapag-sign in. (walang koneksyon ang Wi-Fi mo?)", "telnetAccessDeniedText": "ERROR: ang user ay hindi nagbigay ng access sa telnet.", "timeOutText": "(time out sa ${TIME} segundo)", "touchScreenJoinWarningText": "Sumali ka gamit ang touchscreen.\nKung ito ay isang pagkakamali, i-tap ang 'Menu->Umalis sa Laro' kasama nito.", @@ -940,19 +941,19 @@ "keyboardChangeInstructionsText": "I-double press space para mapalitan ang mga keyboard.", "keyboardNoOthersAvailableText": "Walang ibang mga keyboard na magagamit.", "keyboardSwitchText": "Nagpapalit ng keyboard sa \"${NAME}\".", - "kickOccurredText": "kicked na si ${NAME}", + "kickOccurredText": "na-kicked si ${NAME}", "kickQuestionText": "I-Kick si ${NAME}?", - "kickText": "", + "kickText": "I-Kick", "kickVoteCantKickAdminsText": "Hindi ma-kick ang mga admin.", "kickVoteCantKickSelfText": "Hindi mo ma-kick ng sarili mo.", "kickVoteFailedNotEnoughVotersText": "Hindi marami ang mga manlalaro para sa isang boto.", "kickVoteFailedText": "Nabigo ang kick-vote.", "kickVoteStartedText": "Sinimulan na ang isang kick vote para kay ${NAME}.", - "kickVoteText": "Bumoto sa Kick", + "kickVoteText": "Bumoto sa Pagki-kick", "kickVotingDisabledText": "Naka-disable ang kick voting.", "kickWithChatText": "I-type ang ${YES} sa chat para sa oo at ${NO} para sa hindi.", - "killsTallyText": "${COUNT} na ang pumapatay", - "killsText": "Pumapatay", + "killsTallyText": "${COUNT} ang pinatay", + "killsText": "Pinatay", "kioskWindow": { "easyText": "Madali", "epicModeText": "Mode na Epic", @@ -962,22 +963,22 @@ "singlePlayerExamplesText": "Mga Halimbawa ng Single Player / Co-op", "versusExamplesText": "Halimbawa ng mga Versus" }, - "languageSetText": "Ang wika ay \"${LANGUAGE}\" na ngayon.", + "languageSetText": "Ang wika ay \"${LANGUAGE}\" sa ngayon.", "lapNumberText": "Ikot ${CURRENT}/${TOTAL}", "lastGamesText": "(huling ${COUNT} na laro)", "leaderboardsText": "Mga Leaderboard", "league": { "allTimeText": "Lahat Ng Oras", "currentSeasonText": "Kasalukuyang Season (${NUMBER})", - "leagueFullText": "Liga ng ${NAME}.", + "leagueFullText": "Ligang ${NAME}.", "leagueRankText": "Ranggo ng Liga", "leagueText": "Liga", "rankInLeagueText": "#${RANK}, ${NAME} ${SUFFIX} na Liga", - "seasonEndedDaysAgoText": "Natapos ang season ${NUMBER} araw ang nakalipas.", + "seasonEndedDaysAgoText": "Natapos ang season noing ${NUMBER} na araw.", "seasonEndsDaysText": "Matatapos ang season sa ${NUMBER} (na) araw.", "seasonEndsHoursText": "Matatapos ang season sa ${NUMBER} (na) oras.", "seasonEndsMinutesText": "Matatapos ang season sa ${NUMBER} (na) minuto.", - "seasonText": "Season ${NUMBER}", + "seasonText": "Ika-${NUMBER} na season", "tournamentLeagueText": "Dapat mong maabot ang liga ng ${NAME} upang makapasok sa paligsahan na ito.", "trophyCountsResetText": "Ire-reset ang mga bilang ng tropeo sa susunod na season." }, @@ -988,7 +989,7 @@ "levelText": "Antas ${NUMBER}", "levelUnlockedText": "Unlocked ang Level na Ito!", "livesBonusText": "Bonus ng Buhay", - "loadingText": "Saglit lang...", + "loadingText": "saglit lang...", "loadingTryAgainText": "Naglo-load; subukan muli sa isang saglit…", "macControllerSubsystemBothText": "Pareho (hindi inirerekomenda)", "macControllerSubsystemClassicText": "Klasiko", @@ -1002,9 +1003,9 @@ "endGameText": "Itigil ang Laro", "exitGameText": "Umalis sa Laro", "exitToMenuText": "Balik sa menu?", - "howToPlayText": "Paano maglaro", + "howToPlayText": "Paano Maglaro", "justPlayerText": "(Si ${NAME} lang)", - "leaveGameText": "Ialis sa Laro", + "leaveGameText": "Umalis sa Laro", "leavePartyConfirmText": "Talagang aalis sa party na ito?", "leavePartyText": "Umalis sa Party", "quitText": "Umalis", @@ -1368,6 +1369,7 @@ "tournamentStandingsText": "Mga Paninindigan sa Paligsahan", "tournamentText": "Paligsahan", "tournamentTimeExpiredText": "Na-expire Na Ang Oras Ng Paligsahan", + "tournamentsDisabledWorkspaceText": "Naka-disable ang mga paligsahan kapag aktibo ang mga workspace. \nHuwag munang paganahin muli ang iyong workspace at i-restart upang makipaglaro sa paligsahan.", "tournamentsText": "Mga Paligsahan", "translations": { "characterNames": { @@ -1599,12 +1601,12 @@ "Invalid purchase.": "Di-wastong bilihin", "Invalid tournament entry; score will be ignored.": "Di-wastong entry sa paligsahan; hindi papansinin ang mga iskor.", "Item unlocked!": "Na-unlock ang aytem!", - "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "TINANGGI ANG PAG-LINK. Naglalaman ang ${ACCOUNT}.\nmakabuluhang data na MAWAWALA LAHAT.\nMaaari kang mag-link sa kabaligtaran na pagkakasunud-sunod kung gusto mo\n(at sa halip ay mawala ang data ng account na ITO)", + "LINKING DENIED. ${ACCOUNT} contains\nsignificant data that would ALL BE LOST.\nYou can link in the opposite order if you'd like\n(and lose THIS account's data instead)": "TINANGGI ANG PAG-LINK. ang ${ACCOUNT} na ito\nay may makabuluhang data na maaaring MAWAWALA LAHAT.\nMaaari kang mag-link sa kabaligtaran na pagkakasunud-sunod kung gusto mo\n(at sa halip ay mawala ang data ng account na ITO)", "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "I-link ang account na ${ACCOUNT} sa account na ito?\nMawawala ang lahat ng umiiral na data sa ${ACCOUNT}.\nHindi na ito maaaring bawiin. Sigurado ka ba?", "Max number of playlists reached.": "Naabot na ang maximum na bilang ng mga playlist.", "Max number of profiles reached.": "Naabot na ang maximum na bilang ng mga profile.", "Maximum friend code rewards reached.": "Naabot ang maximum na mga reward sa code ng kaibigan.", - "Message is too long.": "Ang mensahe ay napakataas.", + "Message is too long.": "Ang mensahe ay napakahaba.", "No servers are available. Please try again soon.": "Walang makakuha na mga server. Pakisubukang muli sa lalong madaling oras.", "Profile \"${NAME}\" upgraded successfully.": "Matagumpay na na-upgrade ang profile na \"${NAME}\".", "Profile could not be upgraded.": "Hindi ma-upgrade ang profile.", @@ -1855,7 +1857,7 @@ "workspaceSyncReuseText": "Hindi ma-sync ang ${WORKSPACE}. Muling paggamit ng nakaraang naka-sync na bersyon.", "worldScoresUnavailableText": "Ang scores sa buong mundo ay hindi pa handa", "worldsBestScoresText": "Pinakamahusay na Iskor ng Mundo", - "worldsBestTimesText": "Pinakamahusay na Oras sa Mundo", + "worldsBestTimesText": "Oras ng Pinakamahusay sa Mundo", "xbox360ControllersWindow": { "getDriverText": "Kunin ang Driver", "macInstructions2Text": "Upang gumamit ng mga controller nang wireless, kakailanganin mo rin ang receiver na iyon\nay kasama ang 'Xbox 360 Wireless Controller para sa Windows'.\nPinapayagan ka ng isang receiver na kumonekta hanggang sa 4 na controllers.\n\nMahalaga: Ang mga 3rd-party na receiver ay hindi gagana sa driver na ito;\ntiyaking 'Microsoft' ang nakasulat dito sa iyong receiver, hindi 'XBOX 360'.\nHindi na ibinebenta ng Microsoft ang mga ito nang hiwalay, kaya kakailanganin mong makuha\nyung naka-bundle sa controller or else search ebay.\n\nKung sa tingin mo ay kapaki-pakinabang ito, mangyaring isaalang-alang ang isang donasyon sa\ndeveloper ng driver sa kanilang site.", diff --git a/dist/ba_data/data/languages/french.json b/dist/ba_data/data/languages/french.json index bbf0f95..404efcf 100644 --- a/dist/ba_data/data/languages/french.json +++ b/dist/ba_data/data/languages/french.json @@ -331,12 +331,13 @@ "achievementsRemainingText": "Succès restant à remporter :", "achievementsText": "Succès", "achievementsUnavailableForOldSeasonsText": "Désolé, les spécifications des succès ne sont pas disponibles pour les saisons passées.", + "activatedText": "${THING} activé.", "addGameWindow": { "getMoreGamesText": "Obtenir plus de jeux...", "titleText": "Ajouter un Jeu" }, "allowText": "Autoriser", - "alreadySignedInText": "Votre compte est connecté sur un autre appareil;\n s'il vous plait changez les comptes ou fermez le jeu sur \nles autres appareils et rééssayez", + "alreadySignedInText": "Votre compte est connecté sur un autre appareil;\nveuillez changer de compte ou fermez le jeu sur \nles autres appareils et réessayez.", "apiVersionErrorText": "Impossible de charger le jeu ${NAME}; sa version api est ${VERSION_USED}; nous demandons la version ${VERSION_REQUIRED}.", "audioSettingsWindow": { "headRelativeVRAudioInfoText": "(\"Auto\" s'active seulement quand un casque est branché)", @@ -363,7 +364,7 @@ "boostText": "Accroître", "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} est configuré dans sa propre application.", "buttonText": "bouton", - "canWeDebugText": "Voulez-vous que BombSquad envoie automatiquement un rapport des bugs, \ncrash et certaines informations relatives au jeu au développeur?\n\nCes rapports ne contiendront aucune information personnelle \net aideront à maintenir un jeu sans bug ni ralentissements.", + "canWeDebugText": "Voulez-vous que BombSquad envoie automatiquement un rapport des bugs, \ncrashs et certaines informations relatives au jeu au développeur?\n\nCes rapports ne contiendront aucune information personnelle \net aideront à maintenir un jeu sans bugs ni ralentissements.", "cancelText": "Annuler", "cantConfigureDeviceText": "Désolé, ${DEVICE} ne peut pas être configuré.", "challengeEndedText": "Ce défi est terminé.", @@ -381,7 +382,7 @@ "configureMobileText": "Appareils Mobiles comme Manettes", "configureTouchText": "Configurer l'Ecran Tactile", "ps3Text": "Manettes de PS3", - "titleText": "Mannettes", + "titleText": "Manettes", "wiimotesText": "Wiimotes", "xbox360Text": "Manettes Xbox 360" }, @@ -412,7 +413,7 @@ "ignoredButton4Text": "Bouton Ignoré 4", "ignoredButtonDescriptionText": "(utilisez ceci pour empêcher les boutons 'home' ou 'sync' d’interférer avec l'interface)", "ignoredButtonText": "Bouton Ignoré", - "pressAnyAnalogTriggerText": "Appuyer sur n'importe quelle commande analogique...", + "pressAnyAnalogTriggerText": "Appuyez sur n'importe quelle commande analogique...", "pressAnyButtonOrDpadText": "Appuyez sur n'importe quel bouton ou croix directionnelle...", "pressAnyButtonText": "Appuyez sur n'importe quel bouton...", "pressLeftRightText": "Appuyez à gauche ou à droite...", @@ -475,7 +476,7 @@ "activenessInfoText": "Ce bonus multiplicateur augmente lorsque vous jouez\net baisse quand vous ne jouez pas.", "activityText": "Activité", "campaignText": "Campagne", - "challengesInfoText": "Gagnez des prix en remportant des mini-jeux.\n\nLes prix et la difficulté des niveaux augmentent\ndès qu'un défi est remporté et diminue\nlorsqu'un défi expire ou est abandonné.", + "challengesInfoText": "Gagnez des prix en remportant des mini-jeux.\n\nLes prix et la difficulté des niveaux augmentent\ndès qu'un défi est remporté et diminuent\nlorsqu'un défi expire ou est abandonné.", "challengesText": "Défis", "currentBestText": "Meilleur Score Actuel", "customText": "Personnaliser", @@ -504,11 +505,12 @@ "titleText": "Co-op", "toRankedText": "Avant d'Être Classé", "totalText": "total", - "tournamentInfoText": "Commencez la course au meilleur score\navec les joueurs de votre ligue.\n\nLes prix seront décernés à la fin du tournoi \naux joueurs ayant totalisés le score le plus haut.", + "tournamentInfoText": "Commencez la course au meilleur score\navec les joueurs de votre ligue.\n\nLes prix seront décernés à la fin du tournoi \naux joueurs ayant totalisé le score le plus haut.", "welcome1Text": "Bienvenue à ${LEAGUE}. Vous pouvez améliorer votre\nrang en gagnant des étoiles, en complétant des\nsuccès et en gagnant des trophées durant les tournois.", "welcome2Text": "Vous pouvez aussi gagner des tickets en participant à bien d'autres activités.\nLes tickets sont utiles pour débloquer de nouveaux personnages, \ndes nouvelles cartes, mini-jeux, participer à des tournois et bien plus.", "yourPowerRankingText": "Votre Classement Mondial:" }, + "copyConfirmText": "Copié dans le presse papier.", "copyOfText": "Copie de ${NAME}", "copyText": "Copier", "copyrightText": "© 2013 Eric Froemling", @@ -518,7 +520,7 @@ "creditsWindow": { "additionalAudioArtIdeasText": "Son Additionnel, Design Initial, et Idées par ${NAME}", "additionalMusicFromText": "Musique additionnelle par ${NAME}", - "allMyFamilyText": "Toute ma famille et mes amis qui m'ont aidés à tester le jeu", + "allMyFamilyText": "Toute ma famille et mes amis qui m'ont aidé à tester le jeu", "codingGraphicsAudioText": "Codage, Graphiques, et Audio par ${NAME}", "languageTranslationsText": "Traductions:", "legalText": "Légal:", @@ -542,14 +544,14 @@ "runCPUBenchmarkText": "Lancer le test CPU (processeur)", "runGPUBenchmarkText": "Lancer le test GPU (carte graphique)", "runMediaReloadBenchmarkText": "Lancer le test de Media-Reload", - "runStressTestText": "Test de robustèsse", + "runStressTestText": "Test de robustesse", "stressTestPlayerCountText": "Nombre de Joueurs", - "stressTestPlaylistDescriptionText": "Playlist des tests de robustèsse", + "stressTestPlaylistDescriptionText": "Playlist des tests de robustesse", "stressTestPlaylistNameText": "Nom de la Playlist", "stressTestPlaylistTypeText": "Genre de la Playlist", "stressTestRoundDurationText": "Durée du Niveau", - "stressTestTitleText": "Test de robustèsse", - "titleText": "Tests graphiques/processeur & de robustèsse", + "stressTestTitleText": "Test de robustesse", + "titleText": "Tests graphiques/processeur & de robustesse", "totalReloadTimeText": "Temps de redémarrage total: ${TIME} (référez-vous au registre plus de détails)", "unlockCoopText": "Débloquer les niveaux co-op" }, @@ -569,7 +571,7 @@ "difficultyHardUnlockOnlyText": "Ce niveau ne peut être débloqué qu'en mode difficile.\nPensez-vous être à la hauteur!?!?!", "directBrowserToURLText": "Entrez cette URL dans un navigateur:", "disableRemoteAppConnectionsText": "Désactiver les connexions d'applications-manettes", - "disableXInputDescriptionText": "Permet plus que 4 manettes mais risque à malfonctionner.", + "disableXInputDescriptionText": "Permet plus que 4 manettes mais risque de malfonctionner.", "disableXInputText": "Désactiver XInput", "doneText": "Terminé", "drawText": "Égalité", @@ -588,7 +590,7 @@ "titleText": "Éditeur de Playlist" }, "editProfileWindow": { - "accountProfileInfoText": "Ce profil spécial à un nom \net une icône basés sur votre compte. \n\n${ICONS}\n\nCréez des profils personnalisés pour \nutiliser d'autres noms et icônes.", + "accountProfileInfoText": "Ce profil spécial a un nom \net une icône basés sur votre compte. \n\n${ICONS}\n\nCréez des profils personnalisés pour \nutiliser d'autres noms et icônes.", "accountProfileText": "(profil du compte)", "availableText": "Le nom \"${NAME}\" est disponible.", "changesNotAffectText": "Note: les changements n'auront pas d'effet sur les personnages déjà présent dans le jeu", @@ -609,7 +611,7 @@ "titleEditText": "Éditer ce Profil", "titleNewText": "Nouveau Profil", "unavailableText": "\"${NAME}\" n'est pas disponible; essayez un autre nom.", - "upgradeProfileInfoText": "Ceci vous réserve le droit à un nom de joueur unique dans le monde\net vous permettre d'y assigner une icône personnalisée.", + "upgradeProfileInfoText": "Ceci vous réserve le droit à un nom de joueur unique dans le monde\net vous permet d'y assigner une icône personnalisée.", "upgradeToGlobalProfileText": "Passer à un Profil Mondial" }, "editProfilesAnyTimeText": "(vous pouvez éditer les profils à tout moment dans 'paramètres')", @@ -625,7 +627,7 @@ "deleteConfirmText": "Effacer la Bande Sonore:\n\n'${NAME}'?", "deleteText": "Effacer la \nBande Sonore", "duplicateText": "Dupliquer la\nBande Sonore", - "editSoundtrackText": "Éditeur de Bandes Sonore", + "editSoundtrackText": "Éditeur de Bandes Sonores", "editText": "Modifier la\nBande Sonore", "fetchingITunesText": "chercher des playlists Music App", "musicVolumeZeroWarning": "Attention: le volume de la musique est à 0", @@ -649,7 +651,9 @@ "epicDescriptionFilterText": "${DESCRIPTION} Dans un \"slow-motion\" épique.", "epicNameFilterText": "${NAME} Épique", "errorAccessDeniedText": "accès refusé", + "errorDeviceTimeIncorrectText": "L'heure affichée par votre appareil est décalée de ${HOURS} heures.\nCeci pourrait causer des problèmes.\nVeuillez vérifier l'heure et vos paramètres de fuseau horaire.", "errorOutOfDiskSpaceText": "pas d'éspace sur le disque", + "errorSecureConnectionFailText": "Impossible d'établir une connexion sécurisée au stockage en ligne ; les fonctionnalités en ligne pourraient disfonctionner.", "errorText": "Erreur", "errorUnknownText": "erreur inconnue", "exitGameText": "Quitter ${APP_NAME}?", @@ -716,7 +720,7 @@ "checkingText": "vérification...", "copyCodeConfirmText": "Le code a bien été copié dans le presse-papier.", "copyCodeText": "Copier le code", - "dedicatedServerInfoText": "Pour un meilleur résultat, créer un server dédié. Voir bombsquadgame.com/server pour plus d'info.", + "dedicatedServerInfoText": "Pour un meilleur résultat, créez un server dédié. Voir bombsquadgame.com/server pour plus d'infos.", "disconnectClientsText": "Ceci déconnectera le(s) ${COUNT} joueur(s)\nde votre partie. Êtes-vous sûr?", "earnTicketsForRecommendingAmountText": "Vos amis recevront ${COUNT} tickets si ils essayent le jeu\n(et vous recevrez ${YOU_COUNT} pour chacun d'entre eux qui le feront)", "earnTicketsForRecommendingText": "Partagez le jeu pour \ndes tickets gratuits...", @@ -736,7 +740,7 @@ "getFriendInviteCodeText": "Obtenir un Code pour Inviter mes Amis", "googlePlayDescriptionText": "Invitez des joueurs Google Play à votre partie:", "googlePlayInviteText": "Inviter", - "googlePlayReInviteText": "Le(s) ${COUNT} joueur(s) Google Play seront déconnectés \nsi vous faîtes une autre invitation. Incluez-les dans \nla nouvelle invitation pour continuer de jouer avec eux.", + "googlePlayReInviteText": "Le(s) ${COUNT} joueur(s) Google Play seront déconnectés \nsi vous faites une autre invitation. Incluez-les dans \nla nouvelle invitation pour continuer de jouer avec eux.", "googlePlaySeeInvitesText": "Voir les Invitations", "googlePlayText": "Google Play", "googlePlayVersionOnlyText": "(Version Android / Google Play)", @@ -749,8 +753,8 @@ "joinPublicPartyDescriptionText": "Rejoindre une Partie Publique", "localNetworkDescriptionText": "Rejoindre une partie sur votre réseau (LAN, Bluetooth, Wi-Fi, etc.)", "localNetworkText": "Réseau Local", - "makePartyPrivateText": "Rend Ma Partie Privée", - "makePartyPublicText": "Rend Ma Partie Publique", + "makePartyPrivateText": "Rendre Ma Partie Privée", + "makePartyPublicText": "Rendre Ma Partie Publique", "manualAddressText": "Adresse", "manualConnectText": "Connexion", "manualDescriptionText": "Joindre une partie par adresse:", @@ -827,7 +831,7 @@ "ticketPack4Text": "Pack de tickets géant", "ticketPack5Text": "Énorme pack de tickets", "ticketPack6Text": "Ultime pack de tickets", - "ticketsFromASponsorText": "Gagnez ${COUNT} tickets\nd'un sponsor", + "ticketsFromASponsorText": "Regarder un sponsors \nPour ${COUNT} ticket", "ticketsText": "${COUNT} Tickets", "titleText": "Plus de tickets", "unavailableLinkAccountText": "Désolé , les achats ne sont pas disponibles sur cette plateforme.\nSi vous voulez , vous pouvez lier ce compte à une\nautre plateforme et faire vos achats sur celle-ci.", @@ -838,6 +842,7 @@ "youHaveText": "vous avez ${COUNT} tickets" }, "googleMultiplayerDiscontinuedText": "Désolé, le service multijoueur de Google n'est plus disponible.\nJe travaille sur un moyen de le remplacer aussi vite que possible.\nEn attendant, veuillez essayer une nouvelle méthode de connexion.\n-Eric", + "googlePlayPurchasesNotAvailableText": "Les achats Google Play ne sont pas disponibles.\nVous avez peut-être besoin de mettre à jour votre Google play", "googlePlayText": "Google Play", "graphicsSettingsWindow": { "alwaysText": "Toujours", @@ -1075,7 +1080,7 @@ "modeClassicText": "Mode classique", "modeDemoText": "Mode Demo", "mostValuablePlayerText": "Meilleur joueur", - "mostViolatedPlayerText": "Joueur le plus violé", + "mostViolatedPlayerText": "Joueur le plus violenté", "mostViolentPlayerText": "Joueur le plus violent", "moveText": "Bouger", "multiKillText": "${COUNT}-MEURTRES!!!", @@ -1173,7 +1178,10 @@ "playlistsText": "Playlists", "pleaseRateText": "Si vous aimez ${APP_NAME}, SVP, prenez un moment pour \névaluez ou écrire un commentaire. Ceci nous donnera un \nretour d'info utile pour le développement futur du jeu.\n\nmerci!\n-eric", "pleaseWaitText": "Veuillez patienter...", - "pluginsDetectedText": "Nouveaux plugins détectés. Activez / configurez-les dans les paramètres.", + "pluginClassLoadErrorText": "Une erreur est survenue en chargeant la classe de plugins '${PLUGIN}': ${ERROR}", + "pluginInitErrorText": "Une erreur est survenue en démarrant le plugin '${PLUGIN}' : ${ERROR}", + "pluginsDetectedText": "Nouveaux plugins détectés. Redémarrez l'application pour les activer, ou configurez-les dans les paramètres.", + "pluginsRemovedText": "${NUM} plugin(s) ne sont plus disponibles.", "pluginsText": "Plugins", "practiceText": "Entraînement", "pressAnyButtonPlayAgainText": "Appuyez n'importe quel bouton pour rejouer...", @@ -1436,6 +1444,7 @@ "tournamentStandingsText": "Classements du Tournoi", "tournamentText": "Tournoi", "tournamentTimeExpiredText": "Le temps de ce tournoi a expiré", + "tournamentsDisabledWorkspaceText": "Les tournois sont désactivés lorsque les espaces de travail sont actifs.\nPour réactiver les tournois, désactivez votre espace de travail et redémarrez.", "tournamentsText": "Tournois", "translations": { "characterNames": { @@ -1949,6 +1958,8 @@ "winsPlayerText": "${NAME} a Gagné!", "winsTeamText": "${NAME} a Gagné!", "winsText": "${NAME} a Gagné!", + "workspaceSyncErrorText": "Erreur de synchronisation avec ${WORKSPACE}. Veuillez consulter les logs pour plus de détails.", + "workspaceSyncReuseText": "Impossible de se synchroniser avec ${WORKSPACE}. Réutilisez la version synchronisée précédente.", "worldScoresUnavailableText": "Les scores mondial sont indisponibles.", "worldsBestScoresText": "Meilleurs scores mondiaux", "worldsBestTimesText": "Meilleurs temps mondiaux", diff --git a/dist/ba_data/data/languages/german.json b/dist/ba_data/data/languages/german.json index 7f48b5d..3cd4f13 100644 --- a/dist/ba_data/data/languages/german.json +++ b/dist/ba_data/data/languages/german.json @@ -331,6 +331,7 @@ "achievementsRemainingText": "Fehlende Erfolge:", "achievementsText": "Erfolge", "achievementsUnavailableForOldSeasonsText": "Leider Leistung Besonderheiten nicht für alte Jahreszeiten zur Verfügung.", + "activatedText": "${THING} aktiviert.", "addGameWindow": { "getMoreGamesText": "Hol dir mehr Spiele...", "titleText": "Spiel hinzufügen", @@ -517,6 +518,7 @@ "welcome2Text": "Sie können auch Tickets zu verdienen aus viele der Aktivitäten.\nKarten können verwendet werden, um neue Charaktere, Karten freizuschalten , und\nMini-Spiele , Turniere und mehr geben .", "yourPowerRankingText": "Dein Power Rang:" }, + "copyConfirmText": "In die Zwischenablage kopiert.", "copyOfText": "${NAME} Kopieren", "copyText": "Kopieren", "copyrightText": "© 2013 Eric Froemling", @@ -658,7 +660,9 @@ "epicDescriptionFilterText": "${DESCRIPTION} In epischer Zeitlupe.", "epicNameFilterText": "Episch ${NAME}", "errorAccessDeniedText": "Zugriff verweigert", + "errorDeviceTimeIncorrectText": "Die Zeit deines Gerätes ist um ${HOURS} Stunden verschoben.\nDas kann Probleme verursachen.\nBitte überprüfe deine Zeit und Zeit-Zonen Einstellungen.", "errorOutOfDiskSpaceText": "Nicht genug Speicherplatz", + "errorSecureConnectionFailText": "Nicht möglich, eine sichere Cloud-Verbindung herzustellen; Netzwerk funktionalität kann versagen.", "errorText": "Fehler", "errorUnknownText": "unbekannter Fehler", "exitGameText": "${APP_NAME} verlassen?", @@ -836,7 +840,7 @@ "ticketPack4Text": "Riesiges Ticketpack", "ticketPack5Text": "Kolossales Ticketpack", "ticketPack6Text": "Ultimatives Ticketpack", - "ticketsFromASponsorText": "Bekomme ${COUNT} Tickets\ndurch Werbung", + "ticketsFromASponsorText": "Sehen Sie sich eine Anzeige an\nfür ${COUNT} Tickets", "ticketsText": "${COUNT} Tickets", "titleText": "Hol dir Tickets", "unavailableLinkAccountText": "Sorry, Einkäufe sind auf dieser Plattform nicht verfügbar.\nUm das zu umgehen verlinke deinen Account mit einem Account auf\neiner anderen Plattform, um dort Einkäufe zu machen.", @@ -847,6 +851,7 @@ "youHaveText": "Du hast ${COUNT} Tickets" }, "googleMultiplayerDiscontinuedText": "Sorry, Googles Multiplayerservice ist nicht länger verfügbar.\nIch arbeite so schnell wie möglich an einem Ersatz.\nBis dahin, versuche bitte eine andere Verbindungsmethode.\n-Eric", + "googlePlayPurchasesNotAvailableText": "Google Play-Käufe sind nicht verfügbar.\nMöglicherweise müssen Sie Ihre Store-App aktualisieren.", "googlePlayText": "Google Play", "graphicsSettingsWindow": { "alwaysText": "Immer", @@ -1191,7 +1196,10 @@ "playlistsText": "Playlists", "pleaseRateText": "Wenn dir ${APP_NAME} Spaß macht, nimm dir kurz die Zeit\nund bewerte es oder schreib ein Review. Durch das Feedback\nwird zukünftige Arbeit an dem Spiel unterstützt.\n\nVielen Dank!\n-eric", "pleaseWaitText": "Bitte warte...", + "pluginClassLoadErrorText": "Fehler beim laden der plugin class '${PLUGIN}': ${ERROR}", + "pluginInitErrorText": "Fehler beim einleiten des plugins '${PLUGIN}': ${ERROR}", "pluginsDetectedText": "Neue Plugins erkannt. Neustarten, um sie zu aktivieren oder in den Einstellungen konfigurieren.", + "pluginsRemovedText": "${NUM} plugin(s) wurden nicht mehr gefunden.", "pluginsText": "Plugins", "practiceText": "Übung", "pressAnyButtonPlayAgainText": "Drücke einen Knopf um nochmal zu spielen...", @@ -1459,6 +1467,7 @@ "tournamentStandingsText": "Tournier Tabelle", "tournamentText": "Turnier", "tournamentTimeExpiredText": "Turnierzeit abgelaufen", + "tournamentsDisabledWorkspaceText": "Turniere sind deaktiviert, wenn Arbeitsbereiche aktiv sind.\nUm Turniere wieder zu aktivieren, deaktivieren Sie Ihren Workspace und starten Sie neu.", "tournamentsText": "Turniere", "translations": { "characterNames": { @@ -1979,6 +1988,8 @@ "winsPlayerText": "${NAME} Gewinnt!", "winsTeamText": "${NAME} Gewinnt!", "winsText": "${NAME} gewinnt!", + "workspaceSyncErrorText": "Fehler beim synchronisieren von ${WORKSPACE}. Sieh im log für details.", + "workspaceSyncReuseText": "Kann ${WORKSPACE} nicht synchronisieren. Benutze vorher synchronisierte Version.", "worldScoresUnavailableText": "Weltrangliste ist nicht verfügbar", "worldsBestScoresText": "Beste Punktzahl weltweit", "worldsBestTimesText": "Beste Zeit weltweit", diff --git a/dist/ba_data/data/languages/gibberish.json b/dist/ba_data/data/languages/gibberish.json index 4241678..7033d75 100644 --- a/dist/ba_data/data/languages/gibberish.json +++ b/dist/ba_data/data/languages/gibberish.json @@ -520,6 +520,7 @@ "welcome2Text": "Yz cm alfj fcojwfowiejfo wiejo wfoinoaicoiwefoiwef.\nTickef woioiweofiw efoiauoicoiwefjoaieofaefa\nminf-fizoj , and itner ouacohao,a nd fmofz.", "yourPowerRankingText": "Yrrlz Powe Rnkkffz:" }, + "copyConfirmText": "Cpoew cow owes oC.", "copyOfText": "Copzyz du ${NAME}", "copyText": "Czópy", "copyrightText": "© 2013 Eric Froemling", @@ -661,7 +662,7 @@ "epicDescriptionFilterText": "${DESCRIPTION} Ín ípic slúw mztíon.", "epicNameFilterText": "${NAME} Epícz", "errorAccessDeniedText": "acczlr dnfflz", - "errorDeviceTimeIncorrectText": "Yowruc cowier oowefjwoefj ${HOURS} horewif.\noitwocweojowerjiowejjjfwoef.\nPefjwe wehapeocjjgwghwe w e weofwoefjwe.", + "errorDeviceTimeIncorrectText": "Yowruc cowier ij incorwjeof ${HOURS} horewif.\noitwocweojowerjiowejjjfwoef.\nPefjwe wehapeocjjgwghwe w e weofwoefjwe.", "errorOutOfDiskSpaceText": "orz of dkzk spzlfz", "errorSecureConnectionFailText": "Uweorcjwoef ojcowe werryyeoi nowe; jcnwoeroidfowdffdj.dfsdf.", "errorText": "Errórz", @@ -730,6 +731,7 @@ "checkingText": "chzckinggz..", "copyCodeConfirmText": "Cdf cpodf to clfjoifjz.", "copyCodeText": "Cpoef Cwfdf", + "copyConfirmText": "COpic for cowejwdf.", "dedicatedServerInfoText": "For code wocj woiejfowiejf, loci joweijf owiejfw. Se eocwj efowiejo wcoweijf woeifowoco er.", "disconnectClientsText": "Thz wlzl dicntjf thz ${COUNT} pljflaf (s)\ninc yrrz prthra. Arz yrz fsrru?", "earnTicketsForRecommendingAmountText": "Fofofj oicow ${COUNT} ocwjoe f cow ef woefje\n(aocweo fwjoefi jo${YOU_COUNT} cowiejfowi oie)", @@ -1475,6 +1477,7 @@ "tournamentStandingsText": "Tzewfjwoij Stndfalfjz", "tournamentText": "Tanfowijfowef", "tournamentTimeExpiredText": "Tmcoef Tm Epzoiejfefz", + "tournamentsDisabledWorkspaceText": "Towejowc we wrjw f;aoweahwe aowwej fwoeij woicjwerwer.\nTow c we rapowi f cjqo qpwpi hgpwiejf. nowe wooer wieje wclcoiwjer.", "tournamentsText": "Trzzmfnmflfzzs", "translations": { "characterNames": { diff --git a/dist/ba_data/data/languages/greek.json b/dist/ba_data/data/languages/greek.json index e71cb08..5214a33 100644 --- a/dist/ba_data/data/languages/greek.json +++ b/dist/ba_data/data/languages/greek.json @@ -325,6 +325,7 @@ "achievementsRemainingText": "Υπολειπόμενα Επιτεύγματα:", "achievementsText": "Επιτεύγματα", "achievementsUnavailableForOldSeasonsText": "Συγνώμη, ακριβείς λεπτομέρειες σχετικές με τα επιτεύγματα είναι μη διαθέσιμες για παλαιότερες σεζόν.", + "activatedText": "Το ${THING} ενεργοποιήθηκε.", "addGameWindow": { "getMoreGamesText": "Περισσότερα Παιχνίδια...", "titleText": "Προσθήκη Παιχνιδιού" @@ -496,6 +497,7 @@ "welcome2Text": "Μπορείτε ακόμα να κερδίσετε εισιτήρια με πολλές παρόμοιες δραστηριότητες.\nΤα εισιτήρια μπορούν να χρησιμοποιηθούν για να ξεκλειδώσετε νέους\nχαρακτήρες, χάρτες και μικροπαιχνίδια, να συμμετάσχετε σε τουρνουά, κ.α.", "yourPowerRankingText": "Η Κατάταξη Δύναμής Σας:" }, + "copyConfirmText": "Αντιγράφτηκε στο πρόχειρο.", "copyOfText": "${NAME} Αντίγραφο", "copyText": "αντίγραφο", "createEditPlayerText": "<Δημιουργία/Επεξεργασία Παίκτη>", @@ -623,7 +625,9 @@ "epicDescriptionFilterText": "${DESCRIPTION} Σε επικά αργή κίνηση.", "epicNameFilterText": "${NAME} Επικό", "errorAccessDeniedText": "η πρόσβαση απορρίφθηκε", + "errorDeviceTimeIncorrectText": "Η ώρα της συσκευής σας είναι λάθος ${HOURS} ώρες.\nΑυτο είναι πιθανό να πεοκλέσει προβλήματα.\nΠαρακαλούμε ελέξτε τις ρυθμίσεις ώρας σας.", "errorOutOfDiskSpaceText": "έλλειψη αποθηκευτικού χώρου", + "errorSecureConnectionFailText": "Αδύνατο να δημιουργηθεί ασφαλής σύνδεση cloud, η χρήση του διαδικτύου μπορεί να αποτύχει.", "errorText": "Σφάλμα", "errorUnknownText": "άγνωστο σφάλμα", "exitGameText": "Έξοδος από το ${APP_NAME};", @@ -786,7 +790,7 @@ "ticketPack4Text": "Πακέτο Εισιτηρίων Jumbo", "ticketPack5Text": "Πακέτο Εισιτηρίων Μαμούθ", "ticketPack6Text": "Υπέρτατο Πακέτο Εισιτηρίων", - "ticketsFromASponsorText": "Αποκτήστε ${COUNT} εισιτήρια\nαπό χορηγία", + "ticketsFromASponsorText": "Παρακολουθήστε μια διαφήμηση\nγια ${COUNT} εισιτήρια", "ticketsText": "${COUNT} Εισιτήρια", "titleText": "Αποκτήστε Εισιτήρια", "unavailableLinkAccountText": "Συγνώμη, οι αγορές δεν είναι διαθέσιμες σε αυτή την πλατφόρμα.\nΩς λύση, μπορείτε να δεσμεύσετε αυτόν τον λογαριασμό με έναν\nλογαριασμό από άλλη πλατφόρμα και να αγοράσετε από εκεί.", @@ -797,6 +801,7 @@ "youHaveText": "έχετε ${COUNT} εισιτήρια" }, "googleMultiplayerDiscontinuedText": "Συγνώμη, φαίνεται πως η υπηρεσία πολλών παικτών της Google δεν είναι πλέον διαθέσιμη.\nΠροσπαθώ να βρω αντικατάσταση όσο πιο γρήγορα γίνεται.\nΜέχρι τότε, παρακαλώ δοκιμάστε άλλο τρόπο σύνδεσης.\n-Eric", + "googlePlayPurchasesNotAvailableText": "Οι αγορές Google Play δεν είναι διαθέσιμες.\nΜπορεί να χρειάζεται να ενημερώσετε την εφαρμογή σας.", "googlePlayText": "Google Play", "graphicsSettingsWindow": { "alwaysText": "Πάντα", @@ -1111,7 +1116,10 @@ "playlistsText": "Λίστες Παιχνιδιών", "pleaseRateText": "Εάν απολαμβάνετε το ${APP_NAME}, παρακαλώ σκεφτείτε να αφιερώσετε μιά στιγμή\nγια να το βαθμολογήσετε ή να γράψετε μιά κριτική. Αυτό θα προσφέρει χρήσιμη\nανατροφοδότηση και θα βοηθήσει για την υποστήριξη της μέλλουσας ανάπτυξης.\n\nευχαριστώ!\n-eric", "pleaseWaitText": "Παρακαλώ περιμένετε...", - "pluginsDetectedText": "Νέα πρόσθετο/α εντοπίστηκαν. Ενεργοποίηστε/Διαμορφώστε τα από τις ρυθμίσεις.", + "pluginClassLoadErrorText": "Σφάλμα φορτώνοντας πρόσθετο '${PLUGIN}': ${ERROR}", + "pluginInitErrorText": "Σφάλμα επερξεγάζοντας πρόσθετο '${PLUGIN}': ${ERROR}", + "pluginsDetectedText": "Νέα πρόσθετο/α εντοπίστηκαν. Επανεκκινήστε την εφαρμογή για να τα ενεργοποιήσετε, ή διαμορφώστε τα στις ρυθμίσεις.", + "pluginsRemovedText": "${NUM} πρόσθετο/α δεν εντοπίζονται πια.", "pluginsText": "Πρόσθετα", "practiceText": "Πρακτική", "pressAnyButtonPlayAgainText": "Πατήστε οποιοδήποτε κουμπί για να ξαναπαίξετε...", @@ -1364,6 +1372,7 @@ "tournamentStandingsText": "Πίνακας Κατάταξης Τουρνουά", "tournamentText": "Τουρνουά", "tournamentTimeExpiredText": "Ο Χρόνος του Τουρνουά Έληξε", + "tournamentsDisabledWorkspaceText": "Τα τουρνουά είναι απενεργοποιημένα όταν χόροι εργασίας είναι ενεργοί.\nΓια να το ενεργοποιήσετε, κλείστε τον χώρο εργασίας σας και επανεκκινήστε την εφαρμογή.", "tournamentsText": "Τουρνουά", "translations": { "characterNames": { @@ -1847,6 +1856,8 @@ "winsPlayerText": "Ο Παίκτης ${NAME} Νίκησε!", "winsTeamText": "Η Ομάδα ${NAME} Νίκησε!", "winsText": "${NAME} Νίκησε!", + "workspaceSyncErrorText": "Σφάλμα συνγχρονήζοντας ${WORKSPACE}. Δείτε την καταγραφή για λεπτομέρειες.", + "workspaceSyncReuseText": "Ο χώρος εργασίας ${WORKSPACE} δεν μπορεί να συγχρονιστεί. Η προηγούμενη συγχρονισμένη έκδοση θα χρησιμοποιηθεί.", "worldScoresUnavailableText": "Παγκόσμιες βαθμολογίες μη διαθέσιμες.", "worldsBestScoresText": "Καλύτερες Βαθμολογίες Παγκοσμίως", "worldsBestTimesText": "Καλύτεροι Χρόνοι Παγκοσμίως", diff --git a/dist/ba_data/data/languages/hindi.json b/dist/ba_data/data/languages/hindi.json index e6dd376..d8011cc 100644 --- a/dist/ba_data/data/languages/hindi.json +++ b/dist/ba_data/data/languages/hindi.json @@ -329,6 +329,7 @@ "achievementsRemainingText": "उप्लाब्धियाँ बाकी:", "achievementsText": "उप्लाब्धियाँ", "achievementsUnavailableForOldSeasonsText": "माफ़ करें उपलब्धियों कि जानकारी पुराने सीजन से नहीं है", + "activatedText": "${THING} सक्रिय", "addGameWindow": { "getMoreGamesText": "और गेम्स कि जानकारी पायें", "titleText": "गेम जोड़ें" @@ -499,6 +500,7 @@ "welcome2Text": "आप टिकेट उन्ही गतिविधियाओं से भी कमा सकते हैं | \nटिकेट नए रूप, नक़्शे व छोटे गेम खोलने तथा \nप्रतियोगिता में भाग लेने आदि में काम आ सकते हैं |", "yourPowerRankingText": "आपका सत्ता पद :" }, + "copyConfirmText": "क्लिपबोर्ड पर कॉपी हुआ।", "copyOfText": "${NAME} दूसरा", "copyText": "नकल किजिए", "createEditPlayerText": "<प्लेयर बनाएँ / संपादित करें>", @@ -627,7 +629,9 @@ "epicDescriptionFilterText": "${DESCRIPTION} उत्कृष्ट धीमे गति में।", "epicNameFilterText": "उत्कृष्ट ${NAME}", "errorAccessDeniedText": "अभिगम वर्जित", + "errorDeviceTimeIncorrectText": "आपके उपकरण का समय ${HOURS} घंटे गलत है।\nइससे समस्याएं होने की संभावना है।\nकृपया अपना समय और समय-क्षेत्र सेटिंग की जाँच करें", "errorOutOfDiskSpaceText": "डिस्क पे जगह ख़तम", + "errorSecureConnectionFailText": "सुरक्षित क्लाउड कनेक्शन स्थापित करने में असमर्थ; नेटवर्क कार्यक्षमता विफल हो सकती है", "errorText": "त्रुटी", "errorUnknownText": "अज्ञात त्रुटी", "exitGameText": "${APP_NAME} से निकास करें ?", @@ -792,7 +796,7 @@ "ticketPack4Text": "बहुत बड़ा टिकेट का संग्रह", "ticketPack5Text": "महान टिकेट संग्रह", "ticketPack6Text": "महाकाय टिकेट संग्रह", - "ticketsFromASponsorText": "किसी प्रायोजक \nसे ${COUNT} पायें", + "ticketsFromASponsorText": "एक विज्ञापन देख के \n${COUNT} टिकट प्राप्त करें", "ticketsText": "${COUNT} टिकेट", "titleText": "टिकेट पायें", "unavailableLinkAccountText": "माफ़ करें इस प्लेटफार्म पर खरीदारी नहीं कि जा सकती है |\nएक वैकल्पिक हल के रूप में आप इस खाते को किसी \nऔर प्लेटफार्म के खाते से जोड़ कर खरीदारी कर सकते हैं |", @@ -803,6 +807,7 @@ "youHaveText": "आपके पास ${COUNT} टिकेट हैं" }, "googleMultiplayerDiscontinuedText": "क्षमा करें, गूगल की एक साथ खेलने की सेवा अब उपलब्ध नहीं है। \nमैं जितनी जल्दी हो सके एक प्रतिस्थापन पर काम कर रहा हूं।\nतब तक, कृपया दूसरी जुडने की विधि आज़माएँ। \n-Eric", + "googlePlayPurchasesNotAvailableText": "गूगल प्ले से ख़रीदारी उपलब्ध नहीं हैं।\nआपको अपना स्टोर ऐप अपडेट करना पड़ सकता है।", "googlePlayText": "गूगल प्ले", "graphicsSettingsWindow": { "alwaysText": "हमेशा", @@ -1116,7 +1121,10 @@ "playlistsText": "प्लेलिस्ट", "pleaseRateText": "अगर आपको ${APP_NAME} में मज़ा आ रहा है, \nतो एक क्षण ले कर इसका मूल्यांकन करें | \nयह इस गेम के आगे के विकास का एक बहुत अहम् अंश है | \n\nधन्यवाद !\n-एरिक", "pleaseWaitText": "कृपया प्रतीक्षा करें...", - "pluginsDetectedText": "नए प्लगइन्स पता चले। उन्हें सेटिंग्स में चालू / कॉन्फ़िगर करें।", + "pluginClassLoadErrorText": "प्लगइन क्लास '${PLUGIN}' लोड करने में त्रुटि: ${ERROR}", + "pluginInitErrorText": "प्लगइन '${PLUGIN}' शुरुआत करने में त्रुटि: ${ERROR}", + "pluginsDetectedText": "नए प्लगइन्स पता चले। उन्हें सक्रिय करने के लिए पुनरारंभ करें, या उन्हें सेटिंग्स में कॉन्फ़िगर करें।", + "pluginsRemovedText": "${NUM} प्लगइन्स अब नहीं मिले।", "pluginsText": "प्लगइन्स", "practiceText": "अभ्यास", "pressAnyButtonPlayAgainText": "दोबारा खेलने के लिए कोई भी बटन दबाएँ...", @@ -1368,6 +1376,7 @@ "tournamentStandingsText": "प्रतियोगिता स्टैंडिंग्स", "tournamentText": "प्रतियोगिता", "tournamentTimeExpiredText": "प्रतियोगिता समय समाप्त", + "tournamentsDisabledWorkspaceText": "कार्यस्थान सक्रिय होने पर टूर्नामेंट अक्षम हो जाते हैं।\nटूर्नामेंट को पुन: सक्षम करने के लिए, अपने कार्यक्षेत्र को अक्षम करें और पुनः आरंभ करें।", "tournamentsText": "प्रतियोगिता", "translations": { "characterNames": { @@ -1851,6 +1860,8 @@ "winsPlayerText": "${NAME} विजयी!", "winsTeamText": "${NAME} विजयी!", "winsText": "${NAME} विजयी!", + "workspaceSyncErrorText": "${WORKSPACE} सिंक में त्रुटि। विवरण के लिए लॉग देखें।", + "workspaceSyncReuseText": "${WORKSPACE} सिंक करने में असमर्थ। पिछले समन्वयित संस्करण का पुन: उपयोग होगा।", "worldScoresUnavailableText": "वैश्विक अंक उपलब्ध नहीं", "worldsBestScoresText": "जागतिक सर्वोत्तम स्कोर्स", "worldsBestTimesText": "विश्व का सबसे अधिक समय", diff --git a/dist/ba_data/data/languages/hungarian.json b/dist/ba_data/data/languages/hungarian.json index cf12f2e..59aaaa7 100644 --- a/dist/ba_data/data/languages/hungarian.json +++ b/dist/ba_data/data/languages/hungarian.json @@ -1245,7 +1245,7 @@ "disableCameraShakeText": "Kamera rázást Kikapcsolni", "disableThisNotice": "(kikapcsolhatod ezt a beállítások menüben)", "enablePackageModsDescriptionText": "(engedélyez a plusz helyeket a modoknak, viszont letiltja a hálózati játékot)", - "enablePackageModsText": "Helyi Modok Engedélyezése", + "enablePackageModsText": "Helyi Modok Engedélyezése ", "enterPromoCodeText": "Kód beírása", "forTestingText": "Megj.:ezek az értékek csak tesztek és az alkalmazás bezárásával együtt törlődnek.", "helpTranslateText": "A ${APP_NAME} nem Angol fordításait a közösség végzi.\nHa szeretnél fordítani vagy hibát javítani akkor \nhasználd ezt a linket. Előre is köszönöm!", diff --git a/dist/ba_data/data/languages/indonesian.json b/dist/ba_data/data/languages/indonesian.json index dd784e5..e15ec53 100644 --- a/dist/ba_data/data/languages/indonesian.json +++ b/dist/ba_data/data/languages/indonesian.json @@ -326,6 +326,7 @@ "achievementsRemainingText": "Achievement Tersisa:", "achievementsText": "Achievement", "achievementsUnavailableForOldSeasonsText": "Maaf, spesifik achievement tidak tersedia untuk musim lama.", + "activatedText": "${THING} telah aktif", "addGameWindow": { "getMoreGamesText": "Game Lain...", "titleText": "Tambah Game" @@ -433,13 +434,13 @@ "actionsText": "Aksi", "buttonsText": "tombol", "dragControlsText": "< geser kontrol untuk memposisikannya >", - "joystickText": "Joystick", + "joystickText": "joystick", "movementControlScaleText": "Skala kontrol penggerak", "movementText": "Pergerakan", "resetText": "Kembalikan ke awal", "swipeControlsHiddenText": "Sembunyikan ikon geser", "swipeInfoText": "Model kontrol 'geser' membutuhkan penggunaan sedikit \nnamun membuat mudah untuk bermain tanpa melihat pengontrol", - "swipeText": "Geser", + "swipeText": "geser", "titleText": "Atur layar sentuh" }, "configureItNowText": "Atur sekarang?", @@ -453,7 +454,7 @@ "forIOSText": "Untuk iOS:", "getItForText": "Dapatkan ${REMOTE_APP_NAME} untuk iOS di Apple App Store\natau untuk Android di Google Play Store atau Amazon Appstore", "googlePlayText": "Google Play", - "titleText": "Gunakan Perangkat untuk kntroler:" + "titleText": "Gunakan Perangkat untuk kontroler:" }, "continuePurchaseText": "Lanjutkan untuk ${PRICE}?", "continueText": "Lanjutkan", @@ -496,6 +497,7 @@ "welcome2Text": "Kamu juga dapat mendapatkan tiket dari aktivitas yang sama.\nTiket dapat digunakan untuk membuka karakter baru, peta, dan\nmini games,untuk masuk liga, dan lainnya", "yourPowerRankingText": "Peringkat Kekuatan Kamu:" }, + "copyConfirmText": "Tersalin ke papan klip.", "copyOfText": "Salinan ${NAME}", "copyText": "Salin", "createEditPlayerText": "", @@ -529,20 +531,20 @@ "runMediaReloadBenchmarkText": "Menjalankan Media-Reload Benchmark", "runStressTestText": "Menjalankan test stress", "stressTestPlayerCountText": "Jumlah Pemain", - "stressTestPlaylistDescriptionText": "Stress Test Playlist", - "stressTestPlaylistNameText": "Nama PLaylist", - "stressTestPlaylistTypeText": "Tipe Playlist", + "stressTestPlaylistDescriptionText": "Daftar Putar Stres Tes", + "stressTestPlaylistNameText": "Nama Daftar Putar", + "stressTestPlaylistTypeText": "Tipe Daftar Putar", "stressTestRoundDurationText": "Durasi Permainan", - "stressTestTitleText": "Uji Stress", - "titleText": "Uji Benchmarks % Stress", + "stressTestTitleText": "Uji Stres", + "titleText": "Uji Tolak Ukur % Stres", "totalReloadTimeText": "Total waktu memuat: ${TIME} (lihat log untuk selengkapnya)" }, - "defaultGameListNameText": "Playlist ${PLAYMODE} Semula", - "defaultNewGameListNameText": "Playlist ${PLAYMODE} Ku", + "defaultGameListNameText": "Daftar Putar ${PLAYMODE} Semula", + "defaultNewGameListNameText": "Daftar Putar ${PLAYMODE} Ku", "deleteText": "Hapus", "demoText": "Demo", "denyText": "Tolak", - "desktopResText": "Desktop Res", + "desktopResText": "Resolusi Desktop", "difficultyEasyText": "Mudah", "difficultyHardOnlyText": "Khusus Mode Sulit", "difficultyHardText": "Sulit", @@ -555,31 +557,31 @@ "drawText": "Seri", "duplicateText": "Duplikat", "editGameListWindow": { - "addGameText": "Tambah\nGame", - "cantOverwriteDefaultText": "Tidak dapat mengubah playlist semula!", - "cantSaveAlreadyExistsText": "Playlist dengan nama ini sudah ada!", - "cantSaveEmptyListText": "Tidak dapat menyimpan playlist kosong!", + "addGameText": "Tambah\nPermainan", + "cantOverwriteDefaultText": "Tidak dapat mengubah daftar putar semula!", + "cantSaveAlreadyExistsText": "Daftar Putar dengan nama ini sudah ada!", + "cantSaveEmptyListText": "Tidak dapat menyimpan daftar putar kosong!", "editGameText": "Ubah\nPermainan", - "listNameText": "Nama Playlist", + "listNameText": "Nama Daftar Putar", "nameText": "Nama", "removeGameText": "Hapus\nPermainan", "saveText": "Simpan Daftar", - "titleText": "Pengaturan Playlist" + "titleText": "Penyusun Daftar Putar" }, "editProfileWindow": { - "accountProfileInfoText": "Profil spesial ini mengikuti nama\ndan icon sesuai akun Kamu.\n\n${ICONS}\n\nBuat profil lain untuk menggunakan\nnama dan icon yang berbeda.", + "accountProfileInfoText": "Profil spesial ini mengikuti nama\ndan ikon sesuai akun Kamu.\n\n${ICONS}\n\nBuat profil lain untuk menggunakan\nnama dan ikon yang berbeda.", "accountProfileText": "(Profil Akun)", "availableText": "Nama ini \"${NAME}\" tersedia.", "characterText": "Karakter", "checkingAvailabilityText": "Memeriksa Ketersediaan \"${NAME}\"...", "colorText": "warna", "getMoreCharactersText": "Dapatkan karakter lain...", - "getMoreIconsText": "Dapatkan icon lain...", - "globalProfileInfoText": "profil pemain global dijamin untuk memiliki nama unik\ndi seluruh dunia. Termasuk juga icon lain.", + "getMoreIconsText": "Dapatkan ikon lain...", + "globalProfileInfoText": "profil pemain global dijamin untuk memiliki nama unik\ndi seluruh dunia. Termasuk juga ikon lain.", "globalProfileText": "(Profil Global)", "highlightText": "highlight", - "iconText": "icon", - "localProfileInfoText": "Profile lokal tidak mempunyai ikon dan nama tidak terjamin unik.\nTingkatkan ke profil dunia untuk mendapatkan nama unik dan pemain dapat tambahkan ikon kustom", + "iconText": "ikon", + "localProfileInfoText": "Profile lokal tidak mempunyai ikon dan nama \ntidak terjamin unik. Tingkatkan ke profil global \nuntuk mendapatkan nama unik dan dapat menambahkan ikon kustom.", "localProfileText": "(Profil lokal)", "nameDescriptionText": "Nama Pemain", "nameText": "Nama", @@ -588,7 +590,7 @@ "titleNewText": "Profil Baru", "unavailableText": "\"${NAME}\" tidak tersedia; coba nama lain.", "upgradeProfileInfoText": "Ini akan jadi nama Kamu dalam game ini\ndan memungkinkan Kamu untuk menetapkan ikon kustom.", - "upgradeToGlobalProfileText": "Tingkatkan ke Global Profil" + "upgradeToGlobalProfileText": "Tingkatkan ke Profil Global" }, "editSoundtrackWindow": { "cantDeleteDefaultText": "Kamu tidak dapat menghapus soundtrack asal.", @@ -608,12 +610,12 @@ "newSoundtrackNameText": "Soundtrack saya ${COUNT}", "newSoundtrackText": "Soundtrack Baru:", "newText": "Buat\nSoundtrack", - "selectAPlaylistText": "Pilih Playlist", + "selectAPlaylistText": "Pilih Daftar Putar", "selectASourceText": "Sumber Musik", - "testText": "Test", + "testText": "Tes", "titleText": "Soundtrack", "useDefaultGameMusicText": "Musik Game Asal", - "useITunesPlaylistText": "Music App Playlist", + "useITunesPlaylistText": "Daftar Putar Apl. Musik", "useMusicFileText": "Data Musik (mp3, dll)", "useMusicFolderText": "berkas dari Data Musik" }, @@ -623,8 +625,10 @@ "epicDescriptionFilterText": "${DESCRIPTION} dalam slow-motion yang epik.", "epicNameFilterText": "${NAME} Epik", "errorAccessDeniedText": "akses ditolak", + "errorDeviceTimeIncorrectText": "Waktu di perangkatmu berbeda ${HOURS} jam.\nIni akan menyebabkan masalah.\nSilahkan cek pengaturan jam dan zona waktu anda.", "errorOutOfDiskSpaceText": "media penyimpanan tidak cukup", - "errorText": "Error", + "errorSecureConnectionFailText": "Tidak bisa mendirikan koneksi aman; fungsi jaringan mungkin gagal.", + "errorText": "Kesalahan!", "errorUnknownText": "kesalahan tak teridentifikasi", "exitGameText": "Keluar dari ${APP_NAME}?", "exportSuccessText": "'${NAME}' TEREXPORT", @@ -663,7 +667,7 @@ "newText": "Buat\nPlaylist", "showTutorialText": "Lihat Panduan", "shuffleGameOrderText": "Acak Urutan Game", - "titleText": "Ubah ${TYPE} Playlists" + "titleText": "Ubah ${TYPE} Daftar Putar" }, "gameSettingsWindow": { "addGameText": "Tambah Game" @@ -786,7 +790,7 @@ "ticketPack4Text": "Paket Tiket Jumbo", "ticketPack5Text": "Paket Tiket Raksasa", "ticketPack6Text": "Paket Tiket Berlimpah", - "ticketsFromASponsorText": "Dapatkan ${COUNT} tiket\ndari iklan", + "ticketsFromASponsorText": "Tonton dapat\n${COUNT} tiket", "ticketsText": "${COUNT} tiket", "titleText": "Dapatkan Tiket", "unavailableLinkAccountText": "Maaf, pembelian tidak dapat dilakukan di perangkat ini.\nsebagai antisipasi, kamu dapat menautkan akun ini ke perangkat\nlain dan melakukan pembelian di sana.", @@ -797,6 +801,7 @@ "youHaveText": "kamu memiliki ${COUNT} tiket" }, "googleMultiplayerDiscontinuedText": "Maaf, Google's multiplayer service tidak lagi tersedia.\nSaya sedang bekerja pada penggantian secepat mungkin.\nHingga saat itu, silakan coba metode koneksi lainnya.\n-Eric", + "googlePlayPurchasesNotAvailableText": "Pembayaran Google Play tidak tersedia.\nMungkin perlu memperbarui Playstore anda.", "googlePlayText": "Google Play", "graphicsSettingsWindow": { "alwaysText": "Selalu", @@ -817,7 +822,7 @@ "visualsText": "Visual" }, "helpWindow": { - "bombInfoText": "- Bomb -\nLebih kuat dari Tinju, tapi\ndapat menjadi bom bunuh diri.\ncoba untuk melempar sebelum\nsumbu akan habis.", + "bombInfoText": "- Bomb -\nLebih kuat dari Tinju, tapi\ndapat menjadi bom bunuh diri.\nCoba untuk melempar sebelum\nsumbu akan habis.", "canHelpText": "${APP_NAME} Solusinya!", "controllersInfoText": "Kamu dapat bermain ${APP_NAME} dengan temanmu melalui sebuah\nJaringan, atau kamu dapat bermain dalam perangkat yang sama\njika kamu memiliki kontrol yang cukup. ${APP_NAME} menyediakan\npengontrol digital melalui aplikasi '${REMOTE_APP_NAME}'.\nlihat di Pengaturan -> Kontrol untuk info lebih lanjut.", "controllersInfoTextRemoteOnly": "Anda bisa bermain ${APP_NAME} bersama dengan teman melalui jaringan, \natau kalian semua bisa bermain di perangkat yang sama dengan menggunakan ponsel sebagai pengontrol melalui aplikasi \n'${REMOTE_APP_NAME}' gratis.", @@ -839,7 +844,7 @@ "powerupHealthNameText": "Kotak Medis", "powerupIceBombsDescriptionText": "Lebih lemah dari bom biasa\ntapi membuat musuh Kamu beku,\npanik, gelisah, dan rapuh.", "powerupIceBombsNameText": "Bom Beku", - "powerupImpactBombsDescriptionText": "sedikit lebih lemah dar bom\nbiasa, tapi akan meledak saat terbentur.", + "powerupImpactBombsDescriptionText": "Sedikit lebih lemah dari bom\nbiasa, tapi akan meledak saat terbentur.", "powerupImpactBombsNameText": "Bom Pemicu", "powerupLandMinesDescriptionText": "berisi 3 paket; berguna untuk\nbertahan atau menghentikan\nlangkah musuhmu.", "powerupLandMinesNameText": "Ranjau", @@ -847,11 +852,11 @@ "powerupPunchNameText": "Sarung Tinju", "powerupShieldDescriptionText": "menahan beberapa serangan\nsehingga darah Kamu tidak berkurang.", "powerupShieldNameText": "Energi Pelindung", - "powerupStickyBombsDescriptionText": "lengket ke apapun yang tersentuh.\nSungguh Menjijikan..", + "powerupStickyBombsDescriptionText": "Lengket ke apapun yang tersentuh.\nSungguh Menjijikkan..", "powerupStickyBombsNameText": "Bom Lengket", "powerupsSubtitleText": "Jelas sekali, tidak ada game yang bakal seru tanpa Kekuatan Tambahan:", "powerupsText": "Kekuatan Tambahan", - "punchInfoText": "- Tinju -\nakan lebih berguna saat\nKamu bergerak cepat. jadi lari\ndan berputarlah seperti maddog.", + "punchInfoText": "- Tinju -\nTinju lebih merusak saat\nKamu bergerak cepat. Jadi lari\ndan berputarlah seperti orang gila.", "runInfoText": "- Lari -\nSEMUA tombol dapat digunakan untuk lari. Kecuali tombol pusar Kamu, haha. Lari\ndapat membuat Kamu cepat tapi sulit untuk berbelok, jadi hati-hati dengan jurang.", "someDaysText": "Terkadang, kamu ingin sekali menghajar sesuatu atau menghancurkan sesuatu.", "titleText": "Bantuan ${APP_NAME}", @@ -1065,7 +1070,7 @@ "otherText": "Lainnya...", "outOfText": "(#${RANK} dari ${ALL})", "ownFlagAtYourBaseWarning": "Benderamu harus\nberada di basismu!", - "packageModsEnabledErrorText": "Game yang melalui jaringan tidak diperbolehkan ketika mod-paket-lokal diaktifkan (lihat Pengaturan->Lanjutan)", + "packageModsEnabledErrorText": "Game yang melalui jaringan tidak diperbolehkan ketika mod-paket-lokal diaktifkan (lihat Pengaturan->Lanjutan) ", "partyWindow": { "chatMessageText": "Pesan Obrolan", "emptyText": "acaramu kosong", @@ -1110,7 +1115,10 @@ "playlistsText": "Daftar Putar", "pleaseRateText": "Jika Kamu menyukai ${APP_NAME}, yuk luangkan waktu sejenak untuk menilai dan membubuhkan komentar. Ini akan membantu kami untuk menyempurnakan permainan yang akan datang.\n\nterima kasih!\n-eric", "pleaseWaitText": "Mohon tunggu...", - "pluginsDetectedText": "Plugin baru terdeteksi. Aktifkan/konfigurasikan di pengaturan.", + "pluginClassLoadErrorText": "Error saat memuat class plugin '${PLUGIN}':${ERROR}", + "pluginInitErrorText": "Error saat menjalankan plugin '${PLUGIN}': ${ERROR}", + "pluginsDetectedText": "Plugin baru terdeteksi. Mulai ulang game untuk mengaktifkan pluginnya, atau mengaturnya di pengaturan.", + "pluginsRemovedText": "${NUM} plugin tidak lagi ditemukan.", "pluginsText": "Plugin", "practiceText": "Latihan", "pressAnyButtonPlayAgainText": "Tekan tombol apa saja untuk kembali bermain...", @@ -1219,7 +1227,7 @@ "accountText": "Akun", "advancedText": "Lanjutan", "audioText": "Suara", - "controllersText": "pengontrol", + "controllersText": "Pengontrol", "graphicsText": "Grafik", "playerProfilesMovedText": "NB: Profil-Profil Pemain sudah dipindahkan di jendela Akun di menu utama.", "playerProfilesText": "Profil Pemain", @@ -1236,7 +1244,7 @@ "enablePackageModsText": "Izinkan Paket Mod Lokal", "enterPromoCodeText": "Masukkan Kode", "forTestingText": "NB: jumlah ini hanya untuk tes dan akan hilang saat keluar", - "helpTranslateText": "Translasi ${APP_NAME} selain Bahasa Inggris adalah bantuan \nkomunitas. Jika Kamu ingin membantu atau mengoreksi berkas\ntranslasi, silakan masuk ke situs berikut. Terima kasih!", + "helpTranslateText": "Terjemahan ${APP_NAME} selain Bahasa Inggris adalah bantuan \nkomunitas. Jika Kamu ingin membantu atau mengoreksi berkas\nterjemahan, silahkan masuk ke situs berikut. Terima kasih!", "kickIdlePlayersText": "Keluarkan Pemain Diam", "kidFriendlyModeText": "Mode Dibawah Umur (kekerasan rendah, dll)", "languageText": "Bahasa", @@ -1252,7 +1260,7 @@ "translationFetchErrorText": "status translasi tidak tersedia.", "translationFetchingStatusText": "memeriksa status translasi", "translationInformMe": "Beritahu saya jika bahasa yang saya gunakan harus diperbarui", - "translationNoUpdateNeededText": "bahasa ini sudah terbaharukan; Horeee !", + "translationNoUpdateNeededText": "Bahasa ini sudah yang terbaru; Horeee !", "translationUpdateNeededText": "** bahasa ini perlu diperbaharui! **", "vrTestingText": "Tes VR" }, @@ -1301,7 +1309,7 @@ "holidaySpecialText": "Spesial Liburan", "howToSwitchCharactersText": "pergi ke \"${SETTINGS} -> ${PLAYER_PROFILES}\" untuk mengubah karakter", "howToUseIconsText": "(Buatlah profil pemain global (dalam jendela akun) untuk menggunakan ini)", - "howToUseMapsText": "(gunakan peta ini di tim/playlist bebasmu", + "howToUseMapsText": "(gunakan peta ini di tim/playlist bebasmu)", "iconsText": "Simbol", "loadErrorText": "Tidak dapat memuat halaman.\nCek koneksi internetmu.", "loadingText": "memuat", @@ -1362,6 +1370,7 @@ "tournamentStandingsText": "Hasil Terbaik Turnamen", "tournamentText": "Turnamen", "tournamentTimeExpiredText": "Waktu Turnamen Berakhir", + "tournamentsDisabledWorkspaceText": "Turnamen telah dinonaktifkan saat workspace(plugin/mod) aktif.\nUntuk mengaktifkan turnamen kembali, nonaktifkan dulu workspace anda dan mulai ulang gamenya.", "tournamentsText": "Turnamen", "translations": { "characterNames": { @@ -1416,23 +1425,23 @@ }, "gameDescriptions": { "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Jadilah 'yang terpilih' dalam waktu yang ditentukan.\nBunuh 'yang terpilih' untuk menjadi 'yang terpilih'.", - "Bomb as many targets as you can.": "Bom target sebanyak mungkin.", + "Bomb as many targets as you can.": "Bom target sebanyak mungkin yang kamu bisa.", "Carry the flag for ${ARG1} seconds.": "Bawa bendera selama ${ARG1} detik.", "Carry the flag for a set length of time.": "Bawa bendera dalam waktu yang ditentukan.", "Crush ${ARG1} of your enemies.": "Hancurkan ${ARG1} musuh.", "Defeat all enemies.": "Hancurkan semua musuh.", - "Dodge the falling bombs.": "Yang bersih ya.", + "Dodge the falling bombs.": "Hindari bom-bom yang berjatuhan.", "Final glorious epic slow motion battle to the death.": "Pertarungan slow motion epik hingga kematian menjemput.", "Gather eggs!": "Kumpulkan telur!", "Get the flag to the enemy end zone.": "Bawa bendera sampai ujung lapangan.", - "How fast can you defeat the ninjas?": "Mampukah Kamu menjadi Hokage?", + "How fast can you defeat the ninjas?": "Seberapa cepat kamu bisa mengalahkan ninja-ninja itu?", "Kill a set number of enemies to win.": "Hancurkan sejumlah musuh.", "Last one standing wins.": "Terakhir hidup menang.", "Last remaining alive wins.": "Terakhir hidup menang.", "Last team standing wins.": "Habisi tim lawan.", "Prevent enemies from reaching the exit.": "Tahan musuh jangan sampai finish.", "Reach the enemy flag to score.": "Sentuh bendera lawan untuk skor.", - "Return the enemy flag to score.": "Kembalikan bendera musuh untuk menyekor.", + "Return the enemy flag to score.": "Kembalikan bendera musuh untuk menskor.", "Run ${ARG1} laps.": "Lari ${ARG1} putaran.", "Run ${ARG1} laps. Your entire team has to finish.": "Lari ${ARG1} putaran. Seluruh tim harus mencapai finish.", "Run 1 lap.": "Lari 1 putaran.", @@ -1691,7 +1700,7 @@ "Red": "Merah" }, "tips": { - "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "Lari-lompat-putar-danpukul yang sempurna dan pada waktu yang tepat dapat\nmembunuh hanya dengan sekali serangan dan dapatkan penghargaan dari temanmu.", + "A perfectly timed running-jumping-spin-punch can kill in a single hit\nand earn you lifelong respect from your friends.": "Lari-lompat-putar-dan pukul yang sempurna dan pada waktu yang tepat dapat\nmembunuh hanya dengan sekali serangan dan dapatkan penghargaan dari temanmu.", "Always remember to floss.": "Selalu ingat untuk buang air.", "Create player profiles for yourself and your friends with\nyour preferred names and appearances instead of using random ones.": "Buat profil pemain untuk teman dan dirimu sendiri dengan\nnama dan penampilan yang kamu sukai daripada menggunakan yang acak.", "Curse boxes turn you into a ticking time bomb.\nThe only cure is to quickly grab a health-pack.": "Kotak Terkutuk membuatmu menjadi bom waktu.\nSatu-satunya obat adalah mencari kotak medis.", @@ -1848,6 +1857,8 @@ "winsPlayerText": "${NAME} Menang!", "winsTeamText": "${NAME} Menang!", "winsText": "${NAME} Menang!", + "workspaceSyncErrorText": "Menyinkronkan ke ${WORKSPACE} error. Lihat log untuk lebih detailnya.", + "workspaceSyncReuseText": "Tidak bisa menyinkronkan ${WORKSPACE}. Menggunakan kembali versi sinkronan sebelumnya.", "worldScoresUnavailableText": "Skor Dunia tidak tersedia.", "worldsBestScoresText": "Nilai Terbaik Dunia", "worldsBestTimesText": "Waktu Terbaik Dunia", diff --git a/dist/ba_data/data/languages/italian.json b/dist/ba_data/data/languages/italian.json index e52d42c..9a4dda2 100644 --- a/dist/ba_data/data/languages/italian.json +++ b/dist/ba_data/data/languages/italian.json @@ -510,6 +510,7 @@ "welcome2Text": "Puoi anche guadagnare biglietti con molte attività simili.\nI biglietti possono essere usato per sbloccare nuovi capitoli, mappe e\nmini-giochi, per entrare nei tornei ed altro.", "yourPowerRankingText": "La tua posizione assoluta" }, + "copyConfirmText": "Copiato negli appunti.", "copyOfText": "${NAME} - Copia", "copyText": "Copia", "copyrightText": "© 2013 Eric Froemling", @@ -650,7 +651,9 @@ "epicDescriptionFilterText": "${DESCRIPTION} a rallentatore leggendario.", "epicNameFilterText": "${NAME} Leggendario", "errorAccessDeniedText": "accesso negato", + "errorDeviceTimeIncorrectText": "L'orario del tuo dispositivo è incorretto di ${HOURS} ore.\nCiò può causare problemi.\nControlla il tuo orario e fuso orario impostato.", "errorOutOfDiskSpaceText": "spazio su disco esaurito", + "errorSecureConnectionFailText": "Impossibile stabilire una connessione sicura con il cloud; le funzionalità online possono interrompersi.", "errorText": "Errore", "errorUnknownText": "errore sconosciuto", "exitGameText": "Uscire da ${APP_NAME}?", @@ -826,7 +829,7 @@ "ticketPack4Text": "Pacchetto Biglietti Jumbo", "ticketPack5Text": "Pacchetto Biglietti Mammuth", "ticketPack6Text": "Pacchetto Biglietti Ultimate", - "ticketsFromASponsorText": "Ottieni ${COUNT} biglietti\nda uno sponsor", + "ticketsFromASponsorText": "Guarda una pubblicità\nper ${COUNT} biglietti", "ticketsText": "${COUNT} Biglietti", "titleText": "Ottieni Biglietti", "unavailableLinkAccountText": "Scusa, gli acquisti non sono disponibili su questa piattaforma.\nCome soluzione, puoi collegare questo account ad un'altra \npiattaforma e fare l'acquisto lì.", @@ -837,6 +840,7 @@ "youHaveText": "Hai ${COUNT} biglietti" }, "googleMultiplayerDiscontinuedText": "Mi dispiace, il servizio multiplayer di Google non è più disponibile.\nSto lavorando per sostituirlo il più velocemente possibile.\nFino a quando non troverò una soluzione, prova un altro metodo per connetterti.\n-Eric", + "googlePlayPurchasesNotAvailableText": "Gli acquisti Google Play non sono disponibili.\nPotresti dover aggiornare lo store.", "googlePlayText": "Google Play", "graphicsSettingsWindow": { "alwaysText": "Sempre", @@ -1427,6 +1431,7 @@ "tournamentStandingsText": "Classifica del torneo", "tournamentText": "Torneo", "tournamentTimeExpiredText": "Tempo del torneo esaurito", + "tournamentsDisabledWorkspaceText": "I tornei sono disabilitati quando una o più mod sono attive.\nPer riattivare i tornei, disabilita tutte le mod e riavvia il gioco.", "tournamentsText": "Tornei", "translations": { "characterNames": { diff --git a/dist/ba_data/data/languages/korean.json b/dist/ba_data/data/languages/korean.json index feb7172..9d91e1e 100644 --- a/dist/ba_data/data/languages/korean.json +++ b/dist/ba_data/data/languages/korean.json @@ -327,6 +327,7 @@ "achievementsRemainingText": "남은 업적:", "achievementsText": "업적", "achievementsUnavailableForOldSeasonsText": "죄송합니다만 이전 시즌에 대해서는 업적 정보가 제공되지 않습니다.", + "activatedText": "${THING}가 작동을 시작했습니다.", "addGameWindow": { "getMoreGamesText": "다른 게임 보기...", "titleText": "게임 추가" @@ -624,7 +625,9 @@ "epicDescriptionFilterText": "(에픽 슬로 모션) ${DESCRIPTION}.", "epicNameFilterText": "에픽 ${NAME}", "errorAccessDeniedText": "액세스가 거부됨", + "errorDeviceTimeIncorrectText": "당신의 디바이스 시간은 ${HOURS}시간이나 맞지 않습니다.\n이러면 아마 문제를 불러 이르킬 수 있습니다.\n디바이스의 시간을 현재 시각으로 바꿔 주십시오.", "errorOutOfDiskSpaceText": "디스크 공간 부족", + "errorSecureConnectionFailText": "클라우드 연결 상황이 안전하지 않습니다. 네트워크가 종종 연결 해제 될 수 있습니다.", "errorText": "오류", "errorUnknownText": "알 수 없는 오류", "exitGameText": "${APP_NAME}를 종료하시겠습니까?", @@ -787,7 +790,7 @@ "ticketPack4Text": "점보 티켓 팩", "ticketPack5Text": "매머드 티켓 팩", "ticketPack6Text": "궁극의 티켓 팩", - "ticketsFromASponsorText": "스폰서로부터 티켓\n${COUNT}장 받기", + "ticketsFromASponsorText": "광고 보고\n티켓 ${COUNT}장 받기", "ticketsText": "티켓 ${COUNT}장", "titleText": "티켓 구입", "unavailableLinkAccountText": "죄송합니다만 이 플랫폼에서는 구매할 수 없습니다.\n해결책으로, 이 계정을 다른 플랫폼의 계정에 연동하여\n그곳에서 구매를 진행할 수 있습니다.", @@ -798,6 +801,7 @@ "youHaveText": "보유량: ${COUNT} 티켓" }, "googleMultiplayerDiscontinuedText": "죄송하지만, 구글의 멀티플레이어 서비스는 더이상 이용할수가 없어요.\n지금 대체제에 가능한 빨리 작업중이에요.\n그 때까지는, 다른 접속 방법을 사용해주세요.\n-Eric", + "googlePlayPurchasesNotAvailableText": "구매가 되지 않았습니다.\n아마 스토어 앱을 업데이트 해야 합니다.", "googlePlayText": "Google Play", "graphicsSettingsWindow": { "alwaysText": "언제나", @@ -1108,7 +1112,10 @@ "playlistsText": "플레이 목록", "pleaseRateText": "${APP_NAME} 앱이 마음에 드시면 잠시 시간을 내어\n평가를 하거나 리뷰를 남겨주세요. 저희가 유용한\n피드백을 얻을 수 있고 향후 개발에 도움이 됩니다.\n\n감사합니다!\n-eric", "pleaseWaitText": "잠시만 기다려 주십시오...", - "pluginsDetectedText": "새로운 플러그인 감지됨. 설정에서 활성/설정해 주십시오.", + "pluginClassLoadErrorText": "플러그인(${PLUGIN})을 불러오는 도중에 오류가 생겼습니다. 오류 : ${ERROR}", + "pluginInitErrorText": "플러그인(${PLUGIN})을 실행하는 도중에 오류가 생겼습니다. 오류 : ${ERROR}", + "pluginsDetectedText": "새로운 플러그인이 감지되었습니다. 게임을 재시작 하거나 설정을 바꿔 주십시오.", + "pluginsRemovedText": "${NUM} 플러그인을 더 이상 찾을 수 없습니다.", "pluginsText": "플러그인", "practiceText": "연습", "pressAnyButtonPlayAgainText": "다시 플레이하려면 아무 버튼이나 누르세요...", @@ -1360,6 +1367,7 @@ "tournamentStandingsText": "토너먼트 성적", "tournamentText": "토너먼트", "tournamentTimeExpiredText": "토너먼트 시간이 종료되었습니다", + "tournamentsDisabledWorkspaceText": "워크숍이 가동 중이라 토너먼트를 할 수 없습니다.\n토너먼트를 하려면, 워크숍을 끄고 게임을 재시작 하시오.", "tournamentsText": "토너먼트", "translations": { "characterNames": { @@ -1844,6 +1852,8 @@ "winsPlayerText": "${NAME} 님 승리!", "winsTeamText": "${NAME} 팀 승리!", "winsText": "${NAME} 님 승리!", + "workspaceSyncErrorText": "${WORKSPACE}를 동기화하다가 오류가 났습니다. 로그를 확인하세요.", + "workspaceSyncReuseText": "${WORKSPACE}를 동기화 할 수 없습니다. 마지막으로 동기화 된 버전으로 돌아갑니다.", "worldScoresUnavailableText": "세계 기록을 이용할 수 없습니다.", "worldsBestScoresText": "세계 최고 점수", "worldsBestTimesText": "세계 최고 시간", diff --git a/dist/ba_data/data/languages/persian.json b/dist/ba_data/data/languages/persian.json index c71ab6a..fb0f615 100644 --- a/dist/ba_data/data/languages/persian.json +++ b/dist/ba_data/data/languages/persian.json @@ -499,6 +499,7 @@ "welcome2Text": "همچنین می‌توانید از راه‌های مشابه بلیت جمع‌آوری کنید.\nبلیت‌ها می‌توانند برای باز کردن بازیکنان جدید، نقشه‌ها، مینی‌بازی‌ها یا برای ورود در مسابقه‌ها و موارد\nبیشتر مورد استفاده قرار گیرند.", "yourPowerRankingText": "رتبه‌بندی قدرت شما:" }, + "copyConfirmText": "در حافظه کلیپ بورد شما کپی شد.", "copyOfText": "${NAME} کپی", "copyText": "کپی کردن", "createEditPlayerText": "<ایجاد/ویرایش بازیکن>", @@ -626,7 +627,7 @@ "epicDescriptionFilterText": "در حماسهٔ حرکت آهسته ${DESCRIPTION}", "epicNameFilterText": "${NAME} حماسهٔ", "errorAccessDeniedText": "دسترسی رد شد", - "errorDeviceTimeIncorrectText": "ساعت گوشی ${HOURS} ساعت خاموش بوده‌است.\nممکن است مشکل به‌وجود بیاید.\nلطفاً ساعت و منطقهٔ زمانی گوشی‌تان را بررسی کنید.", + "errorDeviceTimeIncorrectText": "ساعت گوشی‌تان ${HOURS} ساعت خطا دارد.\nممکن است مشکل به‌وجود بیاید.\nلطفاً ساعت و منطقه زمانی گوشی‌تان را بررسی کنید.", "errorOutOfDiskSpaceText": "حافظه جا ندارد", "errorSecureConnectionFailText": "قادر به ایجاد اتصال ابری امن نیست. عملکرد شبکه ممکن است خراب شود.", "errorText": "خطا", @@ -802,6 +803,7 @@ "youHaveText": ".بلیط دارید ${COUNT} شما" }, "googleMultiplayerDiscontinuedText": "متأسفیم ، سرویس چند نفره Google دیگر در دسترس نیست.\nمن در اسرع وقت در حال جایگزینی هستم.\nتا آن زمان ، لطفاً روش اتصال دیگری را امتحان کنید.", + "googlePlayPurchasesNotAvailableText": "خرید های گوگل‌پلی در دسترس نیستند.\nاحتمالا باید برنامه‌ی استور خود را بروز‌رسانی کنید.", "googlePlayText": "گوگل پلی", "graphicsSettingsWindow": { "alwaysText": "همیشه", @@ -844,8 +846,8 @@ "powerupHealthNameText": "جعبه درمان", "powerupIceBombsDescriptionText": "با این بمب های یخی میتونید حریف ها\nرو منجمد و آسیب پذیر کنید ولی بهتر خودتون\nتوی نزدیکی انفجار این بمب ها قرار نگیرین", "powerupIceBombsNameText": "بمب یخی", - "powerupImpactBombsDescriptionText": "بهش میگن بمب ببر تا به چیزی برخورد\nنکنن منفجر نمیشن", - "powerupImpactBombsNameText": "بمب ببر", + "powerupImpactBombsDescriptionText": "بهش میگن بمب فعال‌شونده تا به چیزی برخورد\nنکنن منفجر نمیشن", + "powerupImpactBombsNameText": "بمب فعال‌شونده", "powerupLandMinesDescriptionText": "با این جعبه به شما سه تا مین\nداده میشه که تا وقتی پرتاب بشن\nتا چیزی روشون میخوره منفجر میشن", "powerupLandMinesNameText": "مین زمینی", "powerupPunchDescriptionText": "بهتون دستکش بکس میده و باعث\nمیشه ضربه مشت های قویتری داشته باشید", @@ -1367,6 +1369,7 @@ "tournamentStandingsText": "جدول رده بندی مسابقات", "tournamentText": "جام حذفی", "tournamentTimeExpiredText": "زمان مسابقات پایان یافت", + "tournamentsDisabledWorkspaceText": "وقتی فضاهای کاری فعال هستند، مسابقات غیرفعال می شوند.\n برای فعال کردن مجدد مسابقات، فضای کاری خود را غیرفعال کنید و دوباره راه اندازی کنید.", "tournamentsText": "مسابقات", "translations": { "characterNames": { @@ -1375,11 +1378,11 @@ "Bernard": "برنارد", "Bones": "اسکلت", "Butch": "بوچ", - "Easter Bunny": "Easter خرگوش جشن", + "Easter Bunny": "خرگوش عید پاک", "Flopsy": "فلاپسی", "Frosty": "آدم‌برفی", "Gretel": "گرتل", - "Grumbledorf": "تردست", + "Grumbledorf": "جادوگر", "Jack Morgan": "جک مورگان", "Kronk": "کرانک", "Lee": "لی", @@ -1387,12 +1390,12 @@ "Mel": "مل", "Middle-Man": "کودن", "Minimus": "مینیموس", - "Pascal": "پنگوئن", - "Pixel": "فرشته", + "Pascal": "پاسکال", + "Pixel": "پیکسل", "Sammy Slam": "سامی کشتی‌گیر", "Santa Claus": "بابا نوئل", "Snake Shadow": "سایه ی مار", - "Spaz": "فلج", + "Spaz": "اسپاز", "Taobao Mascot": "تائوبائو", "Todd": "تاد", "Todd McBurton": "تاد", @@ -1590,7 +1593,7 @@ "Can't link 2 accounts of this type.": ".نمی‌توان دو حساب از این نوع را پیوند داد", "Can't link 2 diamond league accounts.": "نمی‌توان حساب دو لیگ الماس را پیوند داد.", "Can't link; would surpass maximum of ${COUNT} linked accounts.": "پیوند نمی‌شود; حداکثر از ${COUNT} پیوند پشتیبانی می‌شود.", - "Cheating detected; scores and prizes suspended for ${COUNT} days.": "تقلب تشخیص داده شد; نمرات و جوایز برای ${COUNT} روز تعلیق شد.", + "Cheating detected; scores and prizes suspended for ${COUNT} days.": "تقلب تشخیص داده شد; امتیازات و جوایز برای ${COUNT} روز تعلیق شد.", "Could not establish a secure connection.": "نمیتوان یک اتصال امن ایجاد کرد", "Daily maximum reached.": "به حداکثر روزانه رسیده است", "Entering tournament...": "ورود به مسابقات ...", @@ -1774,14 +1777,14 @@ "phrase24Text": "باریکلا ! به این میگن انفجار", "phrase25Text": "دیدی اصلا سخت نبود ؟", "phrase26Text": "حالا دیگه مثل یه ببر قوی شدی", - "phrase27Text": "دشمنا منتظرت هستن ... پس این آموزش ها رو به خاطر بسپار", + "phrase27Text": "این آموزش ها رو به یاد داشته باش، و مطمئن باش که زنده برمی‌گردی!", "phrase28Text": "! ببینم چند مرده حلاجی پهلوون", "phrase29Text": "! خدا قوت", - "randomName1Text": "شایان", - "randomName2Text": "بهنام", - "randomName3Text": "کاوه", - "randomName4Text": "مهدی", - "randomName5Text": "بهرام", + "randomName1Text": "فرد", + "randomName2Text": "هری", + "randomName3Text": "بیل", + "randomName4Text": "چاک", + "randomName5Text": "فیل", "skipConfirmText": ".واقعا از آموزش رد می‌شی ؟ هر کلیدی رو بزن تا رد بشیم", "skipVoteCountText": "نفر خواستار رد شدن از آموزش هستند ${TOTAL} نفر از ${COUNT}", "skippingText": "از آموزش می گذریم", diff --git a/dist/ba_data/data/languages/polish.json b/dist/ba_data/data/languages/polish.json index 8aa800d..cf2793b 100644 --- a/dist/ba_data/data/languages/polish.json +++ b/dist/ba_data/data/languages/polish.json @@ -331,6 +331,7 @@ "achievementsRemainingText": "Pozostałe Osiągnięcia:", "achievementsText": "Osiągnięcia", "achievementsUnavailableForOldSeasonsText": "Wybacz, lecz szczegóły osiągnięć nie są dostępne dla starych sezonów.", + "activatedText": "${THING} aktywowane/a", "addGameWindow": { "getMoreGamesText": "Więcej rozgrywek...", "titleText": "Dodaj grę" @@ -647,7 +648,9 @@ "epicDescriptionFilterText": "${DESCRIPTION} Epickie zwolnione tempo.", "epicNameFilterText": "Epicki tryb - ${NAME}", "errorAccessDeniedText": "odmowa dostępu", + "errorDeviceTimeIncorrectText": "Czas na Twoim urządzeniu nie zgadza się o ${HOURS} godziny.\nTo może powodować problemy.\nSprawdź swoje ustawienia czasu i strefy czasowej.", "errorOutOfDiskSpaceText": "brak miejsca na dysku", + "errorSecureConnectionFailText": "Wystąpił błąd z ustanowieniem bezpiecznego połączenia z chmurą; funkcje sieciowe mogą nie działać.", "errorText": "Błąd", "errorUnknownText": "nieznany błąd", "exitGameText": "Wyjść z ${APP_NAME}?", @@ -825,7 +828,7 @@ "ticketPack4Text": "Paczka Kolos kuponów", "ticketPack5Text": "Mamucia paczka kuponów", "ticketPack6Text": "Paczka Ultimate kuponów", - "ticketsFromASponsorText": "Zdobądź ${COUNT} kuponów\nod sponsora", + "ticketsFromASponsorText": "Obejrzyj reklamę\ndla ${COUNT} kuponów", "ticketsText": "${COUNT} kuponów", "titleText": "Zdobądź kupony", "unavailableLinkAccountText": "Niestety, zakupy nie są możliwe na tej platformie. \nJeśli chcesz, możesz połączyć to konto z kontem na innej platformie\ni dokonać zakupu tam.", @@ -836,6 +839,7 @@ "youHaveText": "masz ${COUNT} kuponów" }, "googleMultiplayerDiscontinuedText": "Przepraszam, usługa gry wieloosobowej Google nie jest już dostępna.\nPracuję nad zamiennikiem tak szybko jak potrafię.\nTymczasem proszę o wypróbowanie innej metody połączenia.\n-Eric", + "googlePlayPurchasesNotAvailableText": "Zakupy Google Play niedostępne.\nSpróbuj zaktualizować aplikację Google Play.", "googlePlayText": "Google Play", "graphicsSettingsWindow": { "alwaysText": "Zawsze", @@ -1161,7 +1165,10 @@ "playlistsText": "Listy gier", "pleaseRateText": "Jeśli polubiłeś ${APP_NAME}, proszę o poddanie go ocenie\nlub napisanie krótkiej recenzji. Pozwoli to zebrać przydatne\ninformacje, które pomogą wesprzeć rozwój gry w przyszłości.\n\nDziękuję!\n-Eric", "pleaseWaitText": "Czekaj chwilkę...", + "pluginClassLoadErrorText": "Błąd ładowania klasy pluginu '${PLUGIN}': ${ERROR}", + "pluginInitErrorText": "Błąd inicjowania pluginu '${PLUGIN}': ${ERROR}", "pluginsDetectedText": "Wykryto nowe pluginy. Uruchom ponownie grę, aby je aktywować, lub skonfiguruje je w ustawieniach.", + "pluginsRemovedText": "Usunięto ${NUM} pluginy(ów)", "pluginsText": "Pluginy", "practiceText": "Praktyka", "pressAnyButtonPlayAgainText": "Naciśnij dowolny przycisk aby zagrać ponownie...", @@ -1425,6 +1432,7 @@ "tournamentStandingsText": "Klasyfikacja Turnieju", "tournamentText": "Turniej", "tournamentTimeExpiredText": "Czas Turnieju wygasł", + "tournamentsDisabledWorkspaceText": "Turnieje są wyłączone gdy obszary robocze są aktywne.\nBy włączyć turnieje, wyłącz obszar roboczy i zrestartuj grę.", "tournamentsText": "Turnieje", "translations": { "characterNames": { @@ -1937,6 +1945,8 @@ "winsPlayerText": "${NAME} Wygrywa!", "winsTeamText": "${NAME} Wygrywają!", "winsText": "${NAME} Wygrywa!", + "workspaceSyncErrorText": "Błąd synchronizowania ${WORKSPACE}. Zobacz detale w logu.", + "workspaceSyncReuseText": "Nie można zsynchronizować ${WORKSPACE}. Ponowne użycie zsynchronizowanej wersji.", "worldScoresUnavailableText": "Ogólnoświatowe wyniki niedostępne.", "worldsBestScoresText": "Najlepsze ogólnoświatowe wyniki", "worldsBestTimesText": "Najlepsze ogólnoświatowe czasy", diff --git a/dist/ba_data/data/languages/portuguese.json b/dist/ba_data/data/languages/portuguese.json index a8e78db..de9c23c 100644 --- a/dist/ba_data/data/languages/portuguese.json +++ b/dist/ba_data/data/languages/portuguese.json @@ -658,7 +658,7 @@ "epicDescriptionFilterText": "${DESCRIPTION} em câmera lenta épica.", "epicNameFilterText": "${NAME} épico(a)", "errorAccessDeniedText": "acesso negado", - "errorDeviceTimeIncorrectText": "A hora do seu dispositivo está atrasada/adiantada em ${HOURS} horas.\nIsso poderá causar problemas.\nPor favor cheque as suas configurações de hora e fuso horário.", + "errorDeviceTimeIncorrectText": "A hora do seu dispositivo está incorreta por ${HOURS} horas.\nIsso causará problemas. \nPor-Favor cheque suas configurações de hora e fuso horário.", "errorOutOfDiskSpaceText": "pouco espaço em disco", "errorSecureConnectionFailText": "Não foi possível estabelecer uma conexão segura à nuvem; a funcionalidade da rede pode falhar.", "errorText": "Erro", @@ -1458,6 +1458,7 @@ "tournamentStandingsText": "Classificação do torneio", "tournamentText": "Torneio", "tournamentTimeExpiredText": "O tempo do torneio expirou.", + "tournamentsDisabledWorkspaceText": "Os torneios são desabilitados quando os espaços de trabalho estão ativos.\nPara reativar os torneios, desative seu espaço de trabalho e reinicie.", "tournamentsText": "Torneios", "translations": { "characterNames": { diff --git a/dist/ba_data/data/languages/russian.json b/dist/ba_data/data/languages/russian.json index 88c6eae..00c7bb4 100644 --- a/dist/ba_data/data/languages/russian.json +++ b/dist/ba_data/data/languages/russian.json @@ -1,28 +1,28 @@ { "accountSettingsWindow": { - "accountNameRules": "Имена учетных записей не могут содержать эмодзи или другие специальные символы", + "accountNameRules": "Имена аккаунтов не могут содержать эмодзи или другие специальные символы", "accountProfileText": "(профиль)", "accountsText": "Аккаунты", "achievementProgressText": "Достижения: ${COUNT} из ${TOTAL}", "campaignProgressText": "Прогресс кампании [Сложный режим]: ${PROGRESS}", "changeOncePerSeason": "Вы можете изменить это только раз в сезон.", "changeOncePerSeasonError": "Вы должны подождать до следующего сезона, чтобы изменить это снова (${NUM} дней)", - "customName": "Имя учётной записи", + "customName": "Имя аккаунта", "deviceSpecificAccountText": "Сейчас используется аккаунт имениустройства: ${NAME}", "linkAccountsEnterCodeText": "Введите код", "linkAccountsGenerateCodeText": "Сгенерировать код", "linkAccountsInfoText": "(делиться достижениями с другими платформами)", - "linkAccountsInstructionsNewText": "Чтобы связать две учетные записи, сгенерируйте код на первом\nи введите этот код на втором. Данные из\nвторой учетной записи будут распределены между ними.\n(Данные с первой учетной записи будут потеряны)\n\nВы можете связать ${COUNT} аккаунтов.\n\nВАЖНО: связывайте только собственные учетные записи;\nЕсли вы свяжетесь с аккаунтами друзей, вы не сможете\nодновременно играть онлайн.", + "linkAccountsInstructionsNewText": "Чтобы связать два аккаунта, сгенерируйте код на первом\nи введите этот код на втором. Данные из\nвторого аккаунта будут распределены между ними.\n(Данные из первого будут потеряны)\n\nВы можете связать ${COUNT} аккаунтов.\n\nВАЖНО: связывайте только собственные аккаунты;\nЕсли вы свяжетесь с аккаунтами друзей, вы не сможете\nодновременно играть онлайн.", "linkAccountsInstructionsText": "Для связки двух аккаунтов, создайте код на одном\nиз них и введите код на другом.\nПрогресс и инвентарь будут объединены.\nВы можете связать до ${COUNT} аккаунтов.", "linkAccountsText": "Связать акаунты", "linkedAccountsText": "Привязанные аккаунты:", "nameChangeConfirm": "Вы уверены, что хотите сменить имя аккаунта на ${NAME}?", "notLoggedInText": "<не авторизован>", - "resetProgressConfirmNoAchievementsText": "Это сбросит весь ваш кооперативный прогресс\nи локальные лучшие результаты (кроме билетов).\nЭтот процесс необратим. Вы уверены?", - "resetProgressConfirmText": "Это сбросит весь ваш кооперативный\nпрогресс, достижения и локальные результаты\n(кроме билетов). Этот процесс необратим.\nВы уверены?", + "resetProgressConfirmNoAchievementsText": "Это сбросит весь ваш кооперативный прогресс\nи локальные рекорды (но не билеты).\nЭтот процесс необратим. Вы уверены?", + "resetProgressConfirmText": "Это сбросит весь ваш кооперативный\nпрогресс, достижения и локальные рекорды\n(кроме билетов). Этот процесс необратим.\nВы уверены?", "resetProgressText": "Сбросить прогресс", "setAccountName": "Задать имя аккаунта", - "setAccountNameDesc": "Выберите имя для отображения своей учетной записи.\nВы можете использовать имя одной из ваших связанных\nучетных записей или создать уникальное имя учётной записи.", + "setAccountNameDesc": "Выберите имя для отображения своего аккаунта.\nВы можете использовать имя одного из ваших связанных аккаунтов или создать уникальное имя аккаунта.", "signInInfoText": "Войдите в аккаунт, чтобы собирать билеты, \nсоревноваться онлайн и делиться успехами.", "signInText": "Войти", "signInWithDeviceInfoText": "(стандартный аккаунт только для этого устройства)", @@ -52,11 +52,11 @@ "achievementText": "Достижение", "achievements": { "Boom Goes the Dynamite": { - "description": "Убейте 3 плохих парней с помощью TNT", - "descriptionComplete": "С помощью TNT убито 3 плохих парней", - "descriptionFull": "Убейте 3 плохих парней с помощью TNT на уровне ${LEVEL}", - "descriptionFullComplete": "3 плохих парней убито с помощью TNT на уровне ${LEVEL}", - "name": "Динамит сейчас взорвётся!" + "description": "Убейте 3 негодяев с помощью TNT", + "descriptionComplete": "С помощью TNT убито 3 негодяев", + "descriptionFull": "Убейте 3 негодяев с помощью TNT на уровне ${LEVEL}", + "descriptionFullComplete": "3 негодяя убито с помощью TNT на уровне ${LEVEL}", + "name": "Динамит делает “БУМ”!" }, "Boxer": { "description": "Победите без использования бомб", @@ -68,26 +68,26 @@ "Dual Wielding": { "descriptionFull": "Соединить 2 контроллера (аппарат или приложение)", "descriptionFullComplete": "Соединено 2 контроллера (аппарат или приложение)", - "name": "Двойное оружие" + "name": "Пáрное оружие" }, "Flawless Victory": { - "description": "Победите не получив повреждений", - "descriptionComplete": "Победа без повреждений", - "descriptionFull": "Пройдите уровень ${LEVEL} не получив повреждений", - "descriptionFullComplete": "Уровень ${LEVEL} пройден без повреждений", - "name": "Безупречная победа" + "description": "Победите не получив урона", + "descriptionComplete": "Победа без получения урона", + "descriptionFull": "Пройдите уровень ${LEVEL} не получив урона", + "descriptionFullComplete": "Уровень ${LEVEL} пройден без урона", + "name": "Чистая победа" }, "Free Loader": { - "descriptionFull": "Начать игру каждый сам за себя с 2 и более игроками", - "descriptionFullComplete": "Начата игра каждый сам за себя с 2 и более игроками", + "descriptionFull": "Начать игру “Каждый сам за себя” с 2 и более игроками", + "descriptionFullComplete": "Начата игра “Каждый сам за себя” с 2 и более игроками", "name": "Один в поле воин" }, "Gold Miner": { - "description": "Убейте 6 плохих парней с помощью мин", - "descriptionComplete": "С помощью мин убито 6 плохих парней", - "descriptionFull": "Убейте 6 плохих парней с помощью мин на уровне ${LEVEL}", - "descriptionFullComplete": "6 плохих парней убито с помощью мин на уровне ${LEVEL}", - "name": "Золотой минёр" + "description": "Убейте 6 негодяев с помощью мин", + "descriptionComplete": "С помощью мин убито 6 негодяев", + "descriptionFull": "Убейте 6 негодяев с помощью мин на уровне ${LEVEL}", + "descriptionFullComplete": "6 негодяев убито с помощью мин на уровне ${LEVEL}", + "name": "Сапер-чемпион" }, "Got the Moves": { "description": "Победите без ударов и бомб", @@ -124,8 +124,8 @@ }, "Mine Games": { "description": "Убейте 3 плохих парней с помощью мин", - "descriptionComplete": "С помощью мин убито 3 злодея", - "descriptionFull": "Убейте 3 плохих парней с помощью мин на уровне ${LEVEL}", + "descriptionComplete": "С помощью мин убито 3 негодяя", + "descriptionFull": "Убейте 3 негодяев с помощью мин на уровне ${LEVEL}", "descriptionFullComplete": "С помощью мин убито 3 негодяя на уровне ${LEVEL}", "name": "Игры с минами" }, @@ -176,14 +176,14 @@ "descriptionComplete": "Победа без использования бомб", "descriptionFull": "Пройдите уровень ${LEVEL} без использования бомб", "descriptionFullComplete": "Уровень ${LEVEL} пройден без использования бомб", - "name": "Боксёр профи" + "name": "Боксёр-профи" }, "Pro Football Shutout": { - "description": "Победите в сухую", - "descriptionComplete": "Уровень был пройден в сухую", - "descriptionFull": "Выиграйте матч ${LEVEL} в сухую", - "descriptionFullComplete": "Победа в матче ${LEVEL} в сухую", - "name": "${LEVEL} в сухую" + "description": "Победите всухую", + "descriptionComplete": "Уровень был пройден всухую", + "descriptionFull": "Выиграйте матч ${LEVEL} всухую", + "descriptionFullComplete": "Победа в матче ${LEVEL} всухую", + "name": "${LEVEL} всухую" }, "Pro Football Victory": { "description": "Выиграйте матч", @@ -207,11 +207,11 @@ "name": "Победа на уровне ${LEVEL}" }, "Rookie Football Shutout": { - "description": "Выиграйте, не дав злодеям забить", - "descriptionComplete": "Победа в сухую", - "descriptionFull": "Выиграйте матч ${LEVEL}, не дав злодеям забить", - "descriptionFullComplete": "Победа в матче ${LEVEL} в сухую", - "name": "${LEVEL} в сухую" + "description": "Выиграйте всухую", + "descriptionComplete": "Победа всухую", + "descriptionFull": "Выиграйте матч ${LEVEL}, всухую", + "descriptionFullComplete": "Победа в матче ${LEVEL} всухую", + "name": "${LEVEL} всухую" }, "Rookie Football Victory": { "description": "Выиграйте матч", @@ -258,21 +258,21 @@ "descriptionComplete": "Победа без смертей", "descriptionFull": "Выиграйте уровень ${LEVEL} не умирая", "descriptionFullComplete": "Победа на уровне ${LEVEL} без смертей", - "name": "Остаться в живых" + "name": "Поживем еще малец" }, "Super Mega Punch": { "description": "Нанесите 100% урона одним ударом", "descriptionComplete": "Нанесено 100% урона одним ударом", "descriptionFull": "Нанесите 100% урона одним ударом на уровне ${LEVEL}", "descriptionFullComplete": "100% урона нанесено одним ударом на уровне ${LEVEL}", - "name": "Супер-мега-удар" + "name": "Супер-Мега-Удар" }, "Super Punch": { "description": "Нанесите 50% урона одним ударом", "descriptionComplete": "Нанесено 50% урона одним ударом", "descriptionFull": "Нанесите 50% урона одним ударом на уровне ${LEVEL}", "descriptionFullComplete": "Нанесено 50% урона одним ударом на уровне ${LEVEL}", - "name": "Супер-удар" + "name": "Супер-Удар" }, "TNT Terror": { "description": "Убейте 6 негодяев с помощью TNT", @@ -287,17 +287,17 @@ "name": "Командный игрок" }, "The Great Wall": { - "description": "Остановите всех злодеев", - "descriptionComplete": "Остановлены все злодеи", - "descriptionFull": "Остановите всех злодеев до одного на уровне ${LEVEL}", - "descriptionFullComplete": "Остановлены все злодеи на уровне ${LEVEL}", - "name": "Великая стена" + "description": "Остановите всех негодяев", + "descriptionComplete": "Остановлены все негодяи", + "descriptionFull": "Остановите всех негодяев до одного на уровне ${LEVEL}", + "descriptionFullComplete": "Остановлены все негодяи на уровне ${LEVEL}", + "name": "Великая Стена" }, "The Wall": { - "description": "Остановите всех злодеев", - "descriptionComplete": "Остановлены все злодеи", - "descriptionFull": "Остановите всех злодеев до одного на уровне ${LEVEL}", - "descriptionFullComplete": "Остановлены все злодеи на уровне ${LEVEL}", + "description": "Остановите всех негодяев", + "descriptionComplete": "Остановлены все негодяи", + "descriptionFull": "Остановите всех негодяев до одного на уровне ${LEVEL}", + "descriptionFullComplete": "Остановлены все негодяи на уровне ${LEVEL}", "name": "Стена" }, "Uber Football Shutout": { @@ -334,15 +334,15 @@ "achievementsUnavailableForOldSeasonsText": "К сожалению, подробности достижений не доступны для старых сезонов.", "activatedText": "${THING} активировано.", "addGameWindow": { - "getMoreGamesText": "Еще игр...", + "getMoreGamesText": "Еще игры", "titleText": "Добавить игру" }, "allowText": "Разрешить", - "alreadySignedInText": "На вашем аккаунте играют на другом устройстве;\nпожалуйста зайдите с другого аккаунта или закройте\nигру на другом устройстве и попытайтесь снова.", + "alreadySignedInText": "С вашего аккаунта играют на другом устройстве;\nпожалуйста зайдите с другого аккаунта или закройте\nигру на другом устройстве и попытайтесь снова.", "apiVersionErrorText": "Невозможно загрузить модуль ${NAME}; он предназначен для API версии ${VERSION_USED}; здесь требуется версия ${VERSION_REQUIRED}.", "audioSettingsWindow": { "headRelativeVRAudioInfoText": "(Режим \"Авто\" активируется только при подключении наушников)", - "headRelativeVRAudioText": "Позиционно-зависимое ВР-аудио", + "headRelativeVRAudioText": "Позиционно-зависимое VR-аудио", "musicVolumeText": "Громкость музыки", "soundVolumeText": "Громкость звука", "soundtrackButtonText": "Саундтреки", @@ -369,7 +369,7 @@ "cantConfigureDeviceText": "Извините, ${DEVICE} невозможно настроить.", "challengeEndedText": "Это состязание завершено.", "chatMuteText": "Заглушить чат", - "chatMutedText": "Чат отключен", + "chatMutedText": "Чат заглушен", "chatUnMuteText": "Включить чат", "choosingPlayerText": "<выбор игрока>", "completeThisLevelToProceedText": "Чтобы продолжить, нужно\nпройти этот уровень!", @@ -377,13 +377,13 @@ "configControllersWindow": { "configureControllersText": "Настройка геймпада", "configureGamepadsText": "Настройка контроллеров", - "configureKeyboard2Text": "Настройка клавиатуры P2", + "configureKeyboard2Text": "Настройка клавиатуры игрока 2", "configureKeyboardText": "Настройка клавиатуры", "configureMobileText": "Использовать мобильные устройства в качестве геймпадов", "configureTouchText": "Настройка сенсорного экрана", "ps3Text": "Геймпады PS3™", "titleText": "Геймпады", - "wiimotesText": "Пульт Wii™", + "wiimotesText": "Пульт Wiimote™", "xbox360Text": "Геймпады Xbox 360™" }, "configGamepadSelectWindow": { @@ -418,15 +418,15 @@ "pressAnyButtonText": "Нажмите любую кнопку", "pressLeftRightText": "Нажмите вправо или влево...", "pressUpDownText": "Нажмите вверх или вниз...", - "runButton1Text": "Кнопка для бега 1", - "runButton2Text": "Кнопка для бега 2", - "runTrigger1Text": "Триггер для бега 1", - "runTrigger2Text": "Триггер для бега 2", + "runButton1Text": "Кнопка бега 1", + "runButton2Text": "Кнопка бега 2", + "runTrigger1Text": "Триггер бега 1", + "runTrigger2Text": "Триггер бега 2", "runTriggerDescriptionText": "(аналоговые триггеры позволяют бегать с разной скоростью)", "secondHalfText": "Используйте эту опцию для настройки второй\nполовины устройства \"два геймпада в одном\",\nдля использования в качестве одного геймпада.", "secondaryEnableText": "Включить", "secondaryText": "Второй геймпад", - "startButtonActivatesDefaultDescriptionText": "(выключить, если ваша кнопка \"старт\" работает больше в качестве кнопки \"меню\")", + "startButtonActivatesDefaultDescriptionText": "(выключить, если ваша кнопка \"старт\" работает в качестве кнопки \"меню\")", "startButtonActivatesDefaultText": "Кнопка Старт активирует стандартный виджет", "titleText": "Настройка геймпада", "twoInOneSetupText": "Настройка геймпада 2-в-1", @@ -512,6 +512,7 @@ "welcome2Text": "Вы также можете заработать билеты от многих из тех же видов деятельности.\nБилеты могут быть использованы , чтобы разблокировать новые персонажи , карты и\nмини -игры, чтобы войти турниры, и многое другое.", "yourPowerRankingText": "Ваш ранг:" }, + "copyConfirmText": "Скопировано в буфер обмена", "copyOfText": "Копия ${NAME}", "copyText": "Копия", "copyrightText": "© 2013 Eric Froemling", @@ -541,13 +542,13 @@ "deathsText": "Смерти", "debugText": "отладка", "debugWindow": { - "reloadBenchmarkBestResultsText": "Внимание: для этого теста рекомендуется установить Настройки->Графика->Текстуры на 'Высок.'", + "reloadBenchmarkBestResultsText": "Внимание: для этого теста рекомендуется установить Настройки->Графика->Текстуры на 'Высокий'", "runCPUBenchmarkText": "Запустить тест производительности CPU", "runGPUBenchmarkText": "Запустить тест производительности GPU", "runMediaReloadBenchmarkText": "Запустить тест производительности загрузки медиа", "runStressTestText": "Выполнить тест-нагрузку", "stressTestPlayerCountText": "Количество игроков", - "stressTestPlaylistDescriptionText": "плей-лист нагрузочного испытания", + "stressTestPlaylistDescriptionText": "Плей-лист нагрузочного испытания", "stressTestPlaylistNameText": "Название плей-листа", "stressTestPlaylistTypeText": "Тип плей-листа", "stressTestRoundDurationText": "Продолжительность раунда", @@ -563,7 +564,7 @@ "defaultNewTeamGameListNameText": "Мои командные игры", "defaultTeamGameListNameText": "Стандартные командные игры", "deleteText": "Удалить", - "demoText": "демонстрация", + "demoText": "Демонстрация", "denyText": "Отклонить", "desktopResText": "Разреш. экрана", "difficultyEasyText": "Легкий", @@ -652,7 +653,7 @@ "epicDescriptionFilterText": "${DESCRIPTION} в эпическом замедленном действии.", "epicNameFilterText": "${NAME} в эпическом режиме", "errorAccessDeniedText": "доступ запрещен", - "errorDeviceTimeIncorrectText": "Время на устройстве отстает на ${HOURS} часов.\nЭто вызывает проблемы.\nПожалуйста, проверьте настройки времени и часового пояса.", + "errorDeviceTimeIncorrectText": "Время на устройстве отстает на ${HOURS} часов.\nЭто может вызывать проблемы.\nПожалуйста, проверьте настройки времени и часового пояса.", "errorOutOfDiskSpaceText": "нет места на диске", "errorSecureConnectionFailText": "Ошибка установки безопасного облачного соединения; сетевые функции могут дать сбой.", "errorText": "Ошибка", @@ -751,7 +752,7 @@ "internetText": "Интернет", "inviteAFriendText": "Друзья еще не играют? Пригласи их\nпопробовать и они получат ${COUNT} билетов.", "inviteFriendsText": "Пригласить друзей", - "joinPublicPartyDescriptionText": "Присоединитесь к публичной вечеринке", + "joinPublicPartyDescriptionText": "Присоединитесь к публичному лобби", "localNetworkDescriptionText": "Присоединяйтесь к ближайшему лобби (локальная сеть, Bluetooth, и т.д.)", "localNetworkText": "Локальная сеть", "makePartyPrivateText": "Сделать мое лобби приватным", @@ -783,7 +784,7 @@ "partyStatusJoinableText": "Ваша команда доступна через интернет", "partyStatusNoConnectionText": "Невозможно подключиться к серверу", "partyStatusNotJoinableText": "Ваше лобби недоступно через интернет", - "partyStatusNotPublicText": "Ваше лобби не для всех", + "partyStatusNotPublicText": "Ваше лобби не публично", "pingText": "пинг", "portText": "Порт", "privatePartyCloudDescriptionText": "Частные лобби работают на выделенных облачных серверах; настройка маршрутизатора не требуется.", @@ -875,7 +876,7 @@ "controllersText": "Контроллеры", "controlsSubtitleText": "У вашего дружелюбного персонажа из ${APP_NAME} есть несколько простых действий:", "controlsText": "Управление", - "devicesInfoText": "В ВР-версию ${APP_NAME} можно играть по сети с обычной версией,\nтак что вытаскивайте свои дополнительные телефоны, планшеты\nи компьютеры, и играйте на них. Можно даже подключить\nобычную версию игры к ВР-версии, чтобы позволить\nостальным наблюдать за действием.", + "devicesInfoText": "В VR-версию ${APP_NAME} можно играть по сети с обычной версией,\nтак что вытаскивайте свои дополнительные телефоны, планшеты\nи компьютеры, и играйте на них. Можно даже подключить\nобычную версию игры к VR-версии, чтобы позволить\nостальным наблюдать за действием.", "devicesText": "Устройства", "friendsGoodText": "Бывают полезны. В ${APP_NAME} веселее играть с несколькими игроками;\nподдерживается до 8 игроков одновременно, что приводит нас к:", "friendsText": "Друзья", @@ -888,15 +889,15 @@ "powerupCurseNameText": "Проклятие", "powerupHealthDescriptionText": "Ни за что не догадаетесь.\nВозвращает полное здоровье.", "powerupHealthNameText": "Аптечка", - "powerupIceBombsDescriptionText": "Слабее, чем обычные бомбы\nно делает врагов заморожеными\nи особенно хрупкими.", + "powerupIceBombsDescriptionText": "Слабее, чем обычные бомбы\nно оставляет врагов заморожеными\nи чрезвычайно хрупкими.", "powerupIceBombsNameText": "Ледяные бомбы", "powerupImpactBombsDescriptionText": "Чуть слабее обычных бомб,\nно взрываются при ударе.", - "powerupImpactBombsNameText": "Ударные бомбы", + "powerupImpactBombsNameText": "Моментальные бомбы", "powerupLandMinesDescriptionText": "Выдаются по 3 штуки.\nПолезны для защиты базы или\nусмирения быстроногих врагов.", "powerupLandMinesNameText": "Мины", "powerupPunchDescriptionText": "Делают ваши удары быстрее,\nлучше, сильнее.", "powerupPunchNameText": "Боксерские перчатки", - "powerupShieldDescriptionText": "Немного поглощает урон,\nчтобы вам не навредили.", + "powerupShieldDescriptionText": "Немного поглощает урон,\nвместо вас.", "powerupShieldNameText": "Энергетический щит", "powerupStickyBombsDescriptionText": "Липнут ко всему, чего касаются.\nИ начинается веселье.", "powerupStickyBombsNameText": "Бомбы-липучки", @@ -921,7 +922,7 @@ "internal": { "arrowsToExitListText": "чтобы выйти из списка нажмите ${LEFT} или ${RIGHT}", "buttonText": "кнопка", - "cantKickHostError": "Невозможно выгнать создателя.", + "cantKickHostError": "Невозможно кикнуть создателя.", "chatBlockedText": "${NAME} заблокирован на ${TIME} секунд.", "connectedToGameText": "Вошел в игру '${NAME}'", "connectedToPartyText": "Вошел в лобби ${NAME}!", @@ -976,7 +977,7 @@ "unableToResolveHostText": "Ошибка: невозможно достичь хоста.", "unavailableNoConnectionText": "Сейчас это недоступно (нет интернет соединения?)", "vrOrientationResetCardboardText": "Используйте это, чтобы сбросить ориентации VR.\nЧтобы играть в игру, вам понадобится внешний контроллер.", - "vrOrientationResetText": "Сброс ориентации ВР.", + "vrOrientationResetText": "Сброс ориентации VR.", "willTimeOutText": "(время выйдет при бездействии)" }, "jumpBoldText": "ПРЫЖОК", @@ -986,11 +987,11 @@ "keyboardChangeInstructionsText": "Нажмите на пробел два раза, чтобы сменить раскладку.", "keyboardNoOthersAvailableText": "Нету других раскладок.", "keyboardSwitchText": "Раскладка изменена на \"${NAME}\".", - "kickOccurredText": "${NAME} изгнали.", - "kickQuestionText": "Изгнать ${NAME}?", - "kickText": "Изгнать", - "kickVoteCantKickAdminsText": "Администраторов нельзя выгнать.", - "kickVoteCantKickSelfText": "Вы не можете выгонять самого себя.", + "kickOccurredText": "${NAME} исключили.", + "kickQuestionText": "Исключить ${NAME}?", + "kickText": "Исключить", + "kickVoteCantKickAdminsText": "Администраторов нельзя исключить.", + "kickVoteCantKickSelfText": "Вы не можете исключить самого себя (но можете выйти).", "kickVoteFailedNotEnoughVotersText": "Недостаточно игроков для голосования.", "kickVoteFailedText": "Голосование на вылет не удалось.", "kickVoteStartedText": "Начато голосование за вылет ${NAME}.", @@ -1046,7 +1047,7 @@ "macControllerSubsystemTitleText": "Поддержка контроллера", "mainMenu": { "creditsText": "Благодарности", - "demoMenuText": "Меню примеров", + "demoMenuText": "Меню демо", "endGameText": "Закончить игру", "exitGameText": "Выйти из игры", "exitToMenuText": "Выйти в меню?", @@ -1060,7 +1061,7 @@ "resumeText": "Продолжить", "settingsText": "Настройки" }, - "makeItSoText": "Поехали!", + "makeItSoText": "Да будет так", "mapSelectGetMoreMapsText": "Ещё карт...", "mapSelectText": "Выбрать...", "mapSelectTitleText": "Карты игры ${GAME}", @@ -1083,7 +1084,7 @@ "nameKilledText": "${NAME} убил ${VICTIM}.", "nameNotEmptyText": "Имя не может быть пустым!", "nameScoresText": "${NAME} ведет!", - "nameSuicideKidFriendlyText": "${NAME} убился.", + "nameSuicideKidFriendlyText": "${NAME} случайно убился.", "nameSuicideText": "${NAME} совершил суицид.", "nameText": "Имя", "nativeText": "Разрешение устройства", @@ -1168,7 +1169,7 @@ "playlistNotFoundText": "плей-лист не найден", "playlistText": "Плей-лист", "playlistsText": "Плей-листы", - "pleaseRateText": "Если вам нравится игра ${APP_NAME}, пожалуйста, подумайте о том,\nчтобы оценить ее или написать рецензию. Это обеспечивает полезную\nобратную связь и помогает поддержать дальнейшую разработку.\n\nСпасибо!\n- Эрик", + "pleaseRateText": "Если вам нравится ${APP_NAME}, пожалуйста, подумайте о том,\nчтобы оценить ее или написать рецензию. Это обеспечивает полезную\nобратную связь и помогает поддержать дальнейшую разработку.\n\nСпасибо!\n- Эрик", "pleaseWaitText": "Пожалуйста, подождите...", "pluginClassLoadErrorText": "Ошибка при попытке загрузить класс плагина '${PLUGIN}': ${ERROR}", "pluginInitErrorText": "Ошибка при инициализации плагина '${PLUGIN}': ${ERROR}", @@ -1182,7 +1183,7 @@ "pressAnyKeyButtonPlayAgainText": "Нажмите любую клавишу/кнопку чтобы играть снова...", "pressAnyKeyButtonText": "Нажмите любую клавишу/кнопку чтобы продолжить...", "pressAnyKeyText": "Нажмите любую клавишу...", - "pressJumpToFlyText": "** Чтобы лететь, продолжайте нажимать прыжок **", + "pressJumpToFlyText": "** Чтобы лететь, быстро нажимайте прыжок **", "pressPunchToJoinText": "нажмите УДАР чтобы присоединиться...", "pressToOverrideCharacterText": "нажмите ${BUTTONS} чтобы переопределить своего персонажа", "pressToSelectProfileText": "Нажмите ${BUTTONS} чтобы выбрать игрока", @@ -1195,9 +1196,9 @@ }, "promoSubmitErrorText": "Ошибка отправки кода, проверьте своё интернете соединение", "ps3ControllersWindow": { - "macInstructionsText": "Выключите питание на задней панели PS3, убедитесь, что Bluetooth\nвключен на вашем компьютере, а затем подключите геймпад к Mac\nс помощью кабеля USB для синхронизации. Теперь можно использовать\nкнопку геймпад 'PS' чтобы подключить его к Mac\nв проводном (USB) или беспроводном (Bluetooth) режиме.\n\nНа некоторых системах Mac при синхронизации может потребоватьсякод доступа.\nВ этом случае обратитесь к следующей инструкции или к гуглу.\n\n\n\n\nГеймпады PS3, связанные по беспроводной сети, должны появиться\nв списке устройств в Настройках системы -> Bluetooth. Возможно, вам придется\nудалить их из этого списка, если вы хотите снова использовать их с PS3.\n\nТакже всегда отключайте их от Bluetooth, когда он не используется,\nиначе будут садиться батарейки.\n\nBluetooth должен обрабатывать до 7 подключенных устройств,\nхотя у вас может получиться по-другому.", + "macInstructionsText": "Выключите питание на задней панели PS3, убедитесь, что Bluetooth\nвключен на вашем компьютере, а затем подключите геймпад к Mac\nс помощью кабеля USB для синхронизации. Теперь можно использовать\nкнопку геймпад 'PS' чтобы подключить его к Mac\nв проводном (USB) или беспроводном (Bluetooth) режиме.\n\nНа некоторых системах Mac при синхронизации может потребоваться код доступа.\nВ этом случае обратитесь к следующей инструкции (или к Google).\n\n\n\n\nГеймпады PS3, связанные по беспроводной сети, должны появиться\nв списке устройств в Настройках системы -> Bluetooth. Возможно, вам придется\nудалить их из этого списка, если вы хотите снова использовать их с PS3.\n\nТакже всегда отключайте их от Bluetooth, когда он не используется,\nиначе будут садиться батарейки.\n\nBluetooth должен обрабатывать до 7 подключенных устройств,\nхотя у вас может получиться по-другому.", "ouyaInstructionsText": "Чтобы использовать геймпад PS3 с OUYA, просто подключите его один раз\nс помощью кабеля USB для синхронизации. Это может отключить другие\nгеймпады, тогда нужно перезагрузить OUYA и отсоединить кабель USB.\n\nПосле этого можно использовать кнопку 'PS' геймпада для беспроводного\nподключения. После игры нажмите и удерживайте кнопку 'PS' в течение\n10 секунд чтобы выключить геймпад, в противном случае он может\nостаться включенным и разрядит батарейки.", - "pairingTutorialText": "видео-тьюториал по синхронизации", + "pairingTutorialText": "видео по связыванию", "titleText": "Использование геймпада PS3 с ${APP_NAME}:" }, "publicBetaText": "ОТКРЫТАЯ БЕТА-ВЕРСИЯ", @@ -1218,8 +1219,8 @@ "remainingInTrialText": "осталось в пробной версии", "remoteAppInfoShortText": "Играть в ${APP_NAME} с семьей или друзьями гораздо веселее.\nПодключите один или несколько джойстиков или установите\n${REMOTE_APP_NAME} на свои устройства, чтобы использовать\nих в качестве джойстиков.", "remote_app": { - "app_name": "ДУ BombSquad", - "app_name_short": "ДУBS", + "app_name": "Пульт BombSquad", + "app_name_short": "Пульт BS", "button_position": "Положение кнопки", "button_size": "Размер кнопки", "cant_resolve_host": "Сервер не найден.", @@ -1434,6 +1435,7 @@ "tournamentStandingsText": "Позиции в турнире", "tournamentText": "Турнир", "tournamentTimeExpiredText": "Время турнира истекло", + "tournamentsDisabledWorkspaceText": "Турниры заблокированы пока рабочие пространства включены.\nДля включения турниров, отключите рабочие места и перезапустите игру.", "tournamentsText": "Турниры", "translations": { "characterNames": { @@ -1485,22 +1487,22 @@ "${GAME} Training": "${GAME}: тренировка", "Infinite ${GAME}": "Бесконечный уровень ${GAME}", "Infinite Onslaught": "Бесконечная атака", - "Infinite Runaround": "Бесконечный манёвр", + "Infinite Runaround": "Бесконечная беготня", "Onslaught": "Бесконечная атака", "Onslaught Training": "Атака: тренировка", "Pro ${GAME}": "${GAME} профи", "Pro Football": "Регби профи", "Pro Onslaught": "Атака профи", - "Pro Runaround": "Манёвр профи", + "Pro Runaround": "Беготня профи", "Rookie ${GAME}": "${GAME} для новичков", "Rookie Football": "Регби для новичков", "Rookie Onslaught": "Атака для новичков", "Runaround": "Бесконечный манёвр", "The Last Stand": "Последний рубеж", - "Uber ${GAME}": "Убер ${GAME}", - "Uber Football": "Убер регби", - "Uber Onslaught": "Убер атака", - "Uber Runaround": "Убер манёвр" + "Uber ${GAME}": "Ӱбер ${GAME}", + "Uber Football": "Ӱбер регби", + "Uber Onslaught": "Ӱбер атака", + "Uber Runaround": "Ӱбер беготня" }, "gameDescriptions": { "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Чтобы победить, стань избранным на некоторое время.\nЧтобы стать избранным, убей избранного.", @@ -1564,7 +1566,7 @@ "Chosen One": "Избранный", "Conquest": "Завоевание", "Death Match": "Смертельный бой", - "Easter Egg Hunt": "Охота на пасхальные яйца", + "Easter Egg Hunt": "Сбор пасхальных яиц", "Elimination": "Ликвидация", "Football": "Регби", "Hockey": "Хоккей", @@ -1580,14 +1582,14 @@ }, "inputDeviceNames": { "Keyboard": "Клавиатура", - "Keyboard P2": "Клавиатура P2" + "Keyboard P2": "Клавиатура игрока 2" }, "languages": { "Arabic": "Арабский", "Belarussian": "Белорусский", "Chinese": "Китайский упрощенный", "ChineseTraditional": "Китайский традиционный", - "Croatian": "Харватский", + "Croatian": "Хорватский", "Czech": "Чешский", "Danish": "Датский", "Dutch": "Голландский", @@ -1597,8 +1599,8 @@ "Finnish": "Финский", "French": "Французский", "German": "Немецкий", - "Gibberish": "Абракадабра", - "Greek": "греческий", + "Gibberish": "Чепухейский", + "Greek": "Греческий", "Hindi": "Хинди", "Hungarian": "Венгерский", "Indonesian": "Индонезийский", @@ -1663,7 +1665,7 @@ }, "serverResponses": { "A code has already been used on this account.": "Код уже был активирован на этом аккаунте.", - "A reward has already been given for that address.": "Эта награда уже была выдана на этот ip адрес", + "A reward has already been given for that address.": "Эта награда уже была выдана на этот IP-адрес", "Account linking successful!": "Аккаунт успешно привязан!", "Account unlinking successful!": "Аккаунт успешно отвязан!", "Accounts are already linked.": "Аккаунты уже привязаны.", @@ -1777,9 +1779,9 @@ "Warning to ${NAME}: turbo / button-spamming knocks you out.": "Предупреждение для ${NAME}: за турбо / быстрое повторное нажатие кнопки можно вылететь." }, "teamNames": { - "Bad Guys": "Плохие парни", + "Bad Guys": "Негодяи", "Blue": "Синие", - "Good Guys": "Хорошие парни", + "Good Guys": "Добряки", "Red": "Красные" }, "tips": { @@ -1793,14 +1795,14 @@ "Don't spin for too long; you'll become dizzy and fall.": "Не крутись долго; у тебя закружится голова и ты упадёшь.", "Hold any button to run. (Trigger buttons work well if you have them)": "Для бега нажмите и держите любую кнопку. (Для этого удобны триггеры, если они есть)", "Hold down any button to run. You'll get places faster\nbut won't turn very well, so watch out for cliffs.": "Для бега удерживайте любую кнопку. Бегать, конечно, быстрее,\nзато труднее поворачивать, так что не забывайте про обрывы.", - "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "Ледяные бомбы не очень мощные, но они замораживают\nвсех вокруг, делая их хрупкими и бьющимися.", + "Ice bombs are not very powerful, but they freeze\nwhoever they hit, leaving them vulnerable to shattering.": "Ледяные бомбы не очень мощные, но они замораживают\nвсех вокруг, оставляя их хрупкими и беззащитными.", "If someone picks you up, punch them and they'll let go.\nThis works in real life too.": "Если кто-то вас схатил, бейте, и вас отпустят.\nВ реальной жизни это тоже работает.", "If you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "Если вам не хватает контроллеров, установите приложение '${REMOTE_APP_NAME}' \nна ваши мобильные устройства, чтобы использовать их в качестве контроллеров.", "If you are short on controllers, install the 'BombSquad Remote' app\non your iOS or Android devices to use them as controllers.": "Если не хватает контроллеров, установите приложение 'BombSquad Remote' на\nустройства iOS или Android, чтобы использовать их в качестве контроллеров.", "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.": "Если к вам прилипла липкая бомба, прыгайте и крутитесь. Может повезет\nстряхнуть бомбу или, на худой конец, повеселить окружающих.", "If you kill an enemy in one hit you get double points for it.": "Если убиваешь врага с одного удара, то получаешь двойные очки.", "If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "Если подхватили проклятие, то единственная надежда на выживание\n- это найти аптечку в ближайшие несколько секунд.", - "If you stay in one place, you're toast. Run and dodge to survive..": "Не стой на месте - поджаришься. Беги и уворачивайся чтобы выжить..", + "If you stay in one place, you're toast. Run and dodge to survive..": "Не стой на месте – помрешь. Беги и уворачивайся чтобы выжить..", "If you've got lots of players coming and going, turn on 'auto-kick-idle-players'\nunder settings in case anyone forgets to leave the game.": "Если у вас много игроков, которые приходят и уходят, включите \"автоматически выкидывать\nбездействующих игроков\" в настройках на случай, если кто-то забудет выйти из игры.", "If your device gets too warm or you'd like to conserve battery power,\nturn down \"Visuals\" or \"Resolution\" in Settings->Graphics": "Если ваше устройство нагревается или вы хотите сохранить заряд батареи,\nуменьшите \"Визуальные эффекты\" или \"Разрешение\" в Настройки->Графика", "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "Если картинка прерывистая, попробуйте уменьшить разрешение\nили визуальные эффекты в настройках графики в игре.", @@ -1860,12 +1862,12 @@ "phrase18Text": "В движении бросок получается дальше.", "phrase19Text": "В прыжке бросок выше.", "phrase20Text": "\"Подкрученные\" бомбы летят еще дальше.", - "phrase21Text": "\"Выжидать\" бомбы довольно сложно.", - "phrase22Text": "Черт.", + "phrase21Text": "\"Подогревать\" бомбы довольно сложно.", + "phrase22Text": "Блин нафиг!", "phrase23Text": "Попробуйте \"подогреть\" фитиль секунду или две.", "phrase24Text": "Ура! Хорошо подогрето.", "phrase25Text": "Ну на этом, пожалуй, всё.", - "phrase26Text": "Вперед, на мины!", + "phrase26Text": "Вперед, к победе!", "phrase27Text": "Не забывай эти советы, и ТОЧНО вернешься живым!", "phrase28Text": "...может быть...", "phrase29Text": "Удачи!", @@ -1873,7 +1875,7 @@ "randomName2Text": "Петя", "randomName3Text": "Иннокентий", "randomName4Text": "Шурик", - "randomName5Text": "Пушок", + "randomName5Text": "Виталий", "skipConfirmText": "Пропустить тьюториал? Коснитесь или нажмите кнопку для подтверждения.", "skipVoteCountText": "${COUNT}/${TOTAL} голосов за пропуск", "skippingText": "пропуск обучения...", diff --git a/dist/ba_data/data/languages/spanish.json b/dist/ba_data/data/languages/spanish.json index 4e36269..0467582 100644 --- a/dist/ba_data/data/languages/spanish.json +++ b/dist/ba_data/data/languages/spanish.json @@ -42,7 +42,7 @@ "ticketsText": "Boletos: ${COUNT}", "titleText": "Cuenta", "unlinkAccountsInstructionsText": "Selecciona una cuenta para dejar de enlazar con ella", - "unlinkAccountsText": "Desenlazar cuentas.", + "unlinkAccountsText": "Desenlazar Cuentas", "v2LinkInstructionsText": "Usa este encale para crearte una cuenta o para iniciar sesión", "viaAccount": "(por cuenta ${NAME})", "youAreLoggedInAsText": "Estás conectado como:", @@ -515,7 +515,9 @@ "welcome2Text": "También puedes ganar tickets desde varias actividades similares.\nLos tickets pueden ser usados para desbloquear nuevos personajes,\nmapas y mini juegos, entrar a torneos, y más.", "yourPowerRankingText": "Tu Clasificación de Poder:" }, + "copyConfirmText": "Copiado al portapapeles.", "copyOfText": "${NAME} Copiar", + "copyText": "Copiar", "createAPlayerProfileText": "¿Crear un perfil?", "createEditPlayerText": "", "createText": "Crear", @@ -652,7 +654,7 @@ "epicDescriptionFilterText": "${DESCRIPTION} En cámara lenta épica.", "epicNameFilterText": "${NAME} - Modo épico", "errorAccessDeniedText": "acceso negado", - "errorDeviceTimeIncorrectText": "La hora actual de tu dispositivo es ${HOURS} horas.\nEsto podría causar problemas.\nPorfavor verifica la hora y zona horaria en ajustes.", + "errorDeviceTimeIncorrectText": "La hora actual de tu dispositivo está incorrecta por ${HOURS} horas.\nEsto podría causar problemas.\nPorfavor verifica la hora y zona horaria en ajustes.", "errorOutOfDiskSpaceText": "insuficiente espacio en disco", "errorSecureConnectionFailText": "No se puede establecer una conexión segura en la nube; La red podría estar fallando", "errorText": "Error", @@ -889,11 +891,11 @@ "pickUpInfoText": "- Levanta -\nAlza banderas, enemigos, o cualquier\notra cosa no atornillada al suelo.\nPulsa de nuevo para lanzar.", "pickUpInfoTextScale": 0.6, "powerupBombDescriptionText": "Puedes tirar tres bombas de\nun solo tiro en vez de una sola.", - "powerupBombNameText": "Bombas Triple", + "powerupBombNameText": "Triple-Bombas", "powerupCurseDescriptionText": "Probablemente querrás evitar estos.\n...¿o quizás no?", "powerupCurseNameText": "Maldición", - "powerupHealthDescriptionText": "Restaura toda la salud.\nNunca habrías imaginado.", - "powerupHealthNameText": "Caja de salud", + "powerupHealthDescriptionText": "Restaura toda la salud.\nNunca lo hubieras adivinado.", + "powerupHealthNameText": "Medicina", "powerupIceBombsDescriptionText": "Más débil que las bombas habituales\npero dejan a tus enemigos congelados\ny particularmente frágiles.", "powerupIceBombsNameText": "Bombas de hielo", "powerupImpactBombsDescriptionText": "Levemente más débiles que las bombas\nnormales, pero explotan al impacto.", @@ -903,7 +905,7 @@ "powerupPunchDescriptionText": "Hace que tus golpes sean más duros,\nmás rápidos, mejores, y más fuertes.", "powerupPunchNameText": "Guantes de Boxeo", "powerupShieldDescriptionText": "Absorbe un poco del impacto\npara que tu no tengas que hacerlo.", - "powerupShieldNameText": "Escudo de Energía", + "powerupShieldNameText": "Electro-Escudo", "powerupStickyBombsDescriptionText": "Se adhieren a cualquier cosa.\nEn serio, es demasiado gracioso.", "powerupStickyBombsNameText": "Bombas Pegajosas", "powerupsSubtitleText": "Por supuesto, ningún juego está completo sin poderes extra:", @@ -1084,7 +1086,7 @@ "modeClassicText": "Modo Clásico", "modeDemoText": "Modo De Demostración", "mostValuablePlayerText": "Jugador más Valorado", - "mostViolatedPlayerText": "Jugador más Violado", + "mostViolatedPlayerText": "Jugador más Agredido", "mostViolentPlayerText": "Jugador más Violento", "moveText": "Mover", "multiKillText": "¡¡¡${COUNT}-COMBO!!!", @@ -1450,6 +1452,7 @@ "tournamentStandingsText": "Puestos del Torneo", "tournamentText": "Torneo", "tournamentTimeExpiredText": "Tiempo del Torneo Expirado", + "tournamentsDisabledWorkspaceText": "Los torneos están deshabilitados cuando los espacios de trabajo están activos.\nPara volver a habilitar los torneos, deshabilite su espacio de trabajo y reinicie el juego.", "tournamentsText": "Torneos", "translations": { "characterNames": { @@ -1466,9 +1469,9 @@ "Jack Morgan": "Jack Morgan", "Kronk": "Kronk", "Lee": "Lee", - "Lucky": "Suertudo", + "Lucky": "Lucky", "Mel": "Mel", - "Middle-Man": "Middle-Man", + "Middle-Man": "Intermediario", "Minimus": "Minimus", "Pascal": "Pascal", "Pixel": "Pixel", @@ -1643,21 +1646,21 @@ "Silver": "Plata" }, "mapsNames": { - "Big G": "La Gran G", + "Big G": "Gran G", "Bridgit": "Puentecito", "Courtyard": "Patio Real", - "Crag Castle": "Castillo de Piedra", + "Crag Castle": "Castillo del Risco", "Doom Shroom": "Hongo de la Muerte", "Football Stadium": "Estadio de Fútbol", - "Happy Thoughts": "Pensamientos felices", + "Happy Thoughts": "Sueños Felices", "Hockey Stadium": "Estadio de Hockey", "Lake Frigid": "Lago Frígido", "Monkey Face": "Cara de Mono", - "Rampage": "Medio Tubo", + "Rampage": "Rampa", "Roundabout": "Rotonda", "Step Right Up": "Paso al Frente", "The Pad": "La Plataforma", - "Tip Top": "La Montaña", + "Tip Top": "Montaña", "Tower D": "Torre D", "Zigzag": "Zigzag" }, diff --git a/dist/ba_data/data/languages/tamil.json b/dist/ba_data/data/languages/tamil.json index 1dc3fd0..a12c8a5 100644 --- a/dist/ba_data/data/languages/tamil.json +++ b/dist/ba_data/data/languages/tamil.json @@ -497,6 +497,7 @@ "welcome2Text": "இதே போன்ற பல செயல்களில் இருந்து நீங்கள் டிக்கெட்டுகளைப் பெறலாம்.\nபுதிய எழுத்துக்கள், வரைபடங்கள் மற்றும் பலவற்றைத் திறக்க டிக்கெட்டுகளைப் பயன்படுத்தலாம்\nசிறு விளையாட்டுகள், போட்டிகளில் நுழைய, மற்றும் பல.", "yourPowerRankingText": "உங்கள் சக்தி தரவரிசை:" }, + "copyConfirmText": "கிளிப்போர்டுக்கு நகலெடுக்கப்பட்டது.", "copyOfText": "${NAME} பிரதி", "copyText": "நகல்", "createEditPlayerText": "<பிளேயரை உருவாக்கவும்/திருத்தவும்>", @@ -624,7 +625,7 @@ "epicDescriptionFilterText": "${DESCRIPTION} காவிய மெதுவான இயக்கத்தில்.", "epicNameFilterText": "காவியம் ${NAME}", "errorAccessDeniedText": "அணுகல் மறுக்கப்பட்டது", - "errorDeviceTimeIncorrectText": "உங்கள் சாதனத்தின் நேரம் ${HOURS} மணிநேரம் உள்ளது.\nஇதனால் பிரச்னைகள் ஏற்பட வாய்ப்புள்ளது.\nஉங்கள் நேரம் மற்றும் நேர மண்டல அமைப்புகளைச் சரிபார்க்கவும்.", + "errorDeviceTimeIncorrectText": "உங்கள் சாதனத்தின் நேரம் ${HOURS} மணிநேரம் தவறாக உள்ளது.\nஇதனால் பிரச்னைகள் ஏற்பட வாய்ப்புள்ளது.\nஉங்கள் நேரம் மற்றும் நேர மண்டல அமைப்புகளைச் சரிபார்க்கவும்.", "errorOutOfDiskSpaceText": "வட்டு இடத்திற்கு வெளியே", "errorSecureConnectionFailText": "பாதுகாப்பான கிளவுட் இணைப்பை நிறுவ முடியவில்லை; பிணைய செயல்பாடு தோல்வியடையலாம்.", "errorText": "பிழை", @@ -1368,6 +1369,7 @@ "tournamentStandingsText": "போட்டி நிலைகள்", "tournamentText": "போட்டி", "tournamentTimeExpiredText": "போட்டி நேரம் காலாவதியானது", + "tournamentsDisabledWorkspaceText": "பணியிடங்கள் செயலில் இருக்கும்போது போட்டிகள் முடக்கப்படும்.\nபோட்டிகளை மீண்டும் இயக்க, உங்கள் பணியிடத்தை முடக்கி மீண்டும் தொடங்கவும்.", "tournamentsText": "போட்டிகள்", "translations": { "characterNames": { @@ -1857,7 +1859,7 @@ "worldsBestScoresText": "உலகின் சிறந்த மதிப்பெண்கள்", "worldsBestTimesText": "உலகின் சிறந்த நேரங்கள்", "xbox360ControllersWindow": { - "getDriverText": "Driver ஐ பெற", + "getDriverText": "Driver ஐ பெரு", "macInstructions2Text": "கட்டுப்படுத்திகளை கம்பியில்லாமல் பயன்படுத்த, உங்களுக்கு ரிசீவரும் தேவை\nவிண்டோஸிற்கான எக்ஸ்பாக்ஸ் 360 வயர்லெஸ் கன்ட்ரோலருடன் வருகிறது.\nஒரு ரிசீவர் உங்களை 4 கட்டுப்படுத்திகளை இணைக்க அனுமதிக்கிறது.\n\nமுக்கியமானது: 3 வது தரப்பு பெறுநர்கள் இந்த டிரைவருடன் வேலை செய்ய மாட்டார்கள்;\nஉங்கள் ரிசீவர் அதில் 'மைக்ரோசாப்ட்' என்று கூறுவதை உறுதி செய்து கொள்ளுங்கள், 'எக்ஸ்பாக்ஸ் 360' அல்ல.\nமைக்ரோசாப்ட் இனி தனித்தனியாக விற்காது, எனவே நீங்கள் பெற வேண்டும்\nகட்டுப்பாட்டாளருடன் தொகுக்கப்பட்ட ஒன்று அல்லது வேறு ஈபேயைத் தேடுங்கள்.\n\nஇது உங்களுக்கு பயனுள்ளதாக இருந்தால், தயவுசெய்து ஒரு நன்கொடையைக் கருத்தில் கொள்ளவும்\nஅவரது தளத்தில் டிரைவர் டெவலப்பர்.", "macInstructionsText": "எக்ஸ்பாக்ஸ் 360 கட்டுப்படுத்திகளைப் பயன்படுத்த, நீங்கள் நிறுவ வேண்டும்\nமேக் டிரைவர் கீழே உள்ள இணைப்பில் கிடைக்கிறது.\nஇது கம்பி மற்றும் வயர்லெஸ் கட்டுப்படுத்திகளுடன் வேலை செய்கிறது.", "ouyaInstructionsText": "BombSquad உடன் கம்பி எக்ஸ்பாக்ஸ் 360 கட்டுப்படுத்திகளைப் பயன்படுத்த, வெறுமனே\nஅவற்றை உங்கள் சாதனத்தின் USB போர்ட்டில் செருகவும். நீங்கள் ஒரு USB ஹப் பயன்படுத்தலாம்\nபல கட்டுப்படுத்திகளை இணைக்க.\n\nவயர்லெஸ் கன்ட்ரோலர்களைப் பயன்படுத்த உங்களுக்கு வயர்லெஸ் ரிசீவர் தேவை,\n\"விண்டோஸிற்கான எக்ஸ்பாக்ஸ் 360 வயர்லெஸ் கன்ட்ரோலர்\" இன் ஒரு பகுதியாக கிடைக்கிறது\nதொகுப்பு அல்லது தனித்தனியாக விற்கப்படுகிறது. ஒவ்வொரு ரிசீவரும் USB போர்ட்டில் செருகப்படுகிறது மற்றும்\n4 வயர்லெஸ் கட்டுப்படுத்திகளை இணைக்க உங்களை அனுமதிக்கிறது.", diff --git a/dist/ba_data/data/languages/thai.json b/dist/ba_data/data/languages/thai.json index bcd6c11..2240af5 100644 --- a/dist/ba_data/data/languages/thai.json +++ b/dist/ba_data/data/languages/thai.json @@ -325,6 +325,7 @@ "achievementsRemainingText": "ความสำเร็จที่ยังเหลืออยู่:", "achievementsText": "ความสำเร็จ", "achievementsUnavailableForOldSeasonsText": "ขออภัย, ความสำเร็จนี้ไม่ได้มีอยู่ในฤดูกาลเก่า", + "activatedText": "เปิดใช้งาน ${THING} แล้ว", "addGameWindow": { "getMoreGamesText": "รับเกมเพิ่มเติม", "titleText": "เพิ่มเกม" @@ -622,7 +623,9 @@ "epicDescriptionFilterText": "${DESCRIPTION} ในการเคลื่อนไหวที่ช้ามากๆ", "epicNameFilterText": "${NAME} แบบช้ามหากาฬ", "errorAccessDeniedText": "การเข้าถึงถูกปฏิเสธ", + "errorDeviceTimeIncorrectText": "เวลาของอุปกรณ์ของคุณปิดลง ${HOURS} ชั่วโมง\nมีแนวโน้มที่จะทำให้เกิดปัญหา\nโปรดตรวจสอบการตั้งค่าเวลาและเขตเวลาของคุณ", "errorOutOfDiskSpaceText": "พื้นที่ว่างในเครื่องหมด", + "errorSecureConnectionFailText": "ไม่สามารถสร้างการเชื่อมต่อระบบคลาวด์ที่ปลอดภัยได้ การทำงานของเครือข่ายอาจล้มเหลว", "errorText": "ข้อผิดพลาด", "errorUnknownText": "ข้อผิดพลาดที่ไม่รู้จัก", "exitGameText": "จะออกจาก ${APP_NAME} หรือไม่?", @@ -785,7 +788,7 @@ "ticketPack4Text": "แพ็กตั๋วจัมโบ้", "ticketPack5Text": "แพ็กตั๋วแมมมอธ", "ticketPack6Text": "แพ็กตั๋วสูงสุด", - "ticketsFromASponsorText": "รับตั๋ว ${COUNT} ใบ\nจากสปอนเซอร์", + "ticketsFromASponsorText": "ดูโฆษณา\nเพื่อรับตั๋ว ${COUNT} ใบ", "ticketsText": "ตั๋ว ${COUNT} ใบ", "titleText": "รับตั๋ว", "unavailableLinkAccountText": "ขออภัย ไม่สามารถซื้อได้บนแพลตฟอร์มนี้\nวิธีแก้ปัญหา คุณสามารถเชื่อมโยงบัญชีนี้กับบัญชีบน\nแพลตฟอร์มอื่นและทำการซื้อที่นั่น", @@ -796,6 +799,7 @@ "youHaveText": "คุณมีตั๋ว ${COUNT} ใบ" }, "googleMultiplayerDiscontinuedText": "ขออภัย บริการผู้เล่นหลายคนของ Google ไม่มีให้บริการอีกต่อไป\nฉันกำลังดำเนินการเปลี่ยนให้เร็วที่สุด\nในระหว่างนี้ โปรดลองวิธีการเชื่อมต่ออื่น\n-เอริค", + "googlePlayPurchasesNotAvailableText": "ไม่สามารถซื้อด้วย Google Play ได้\nคุณอาจต้องอัปเดตแอปร้านค้าของคุณ", "googlePlayText": "Google Play", "graphicsSettingsWindow": { "alwaysText": "ตลอด", @@ -1106,7 +1110,10 @@ "playlistsText": "เพลย์ลิส", "pleaseRateText": "หากคุณชอบ ${APP_NAME} โปรดพิจารณาใช้ a\nและให้คะแนนหรือเขียนรีวิว นี้ให้\nข้อเสนอแนะที่เป็นประโยชน์และช่วยสนับสนุนการพัฒนาในอนาคต\n\nขอบใจ!\n-eric", "pleaseWaitText": "โปรดรอ...", - "pluginsDetectedText": "ตรวจพบปลั๊กอินใหม่ เปิด/กำหนดค่าได้ในการตั้งค่า", + "pluginClassLoadErrorText": "เกิดข้อผิดพลาดในการโหลดคลาสปลั๊กอิน '${PLUGIN}': ${ERROR}", + "pluginInitErrorText": "เกิดข้อผิดพลาดในการเริ่มต้นปลั๊กอิน '${PLUGIN}': ${ERROR}", + "pluginsDetectedText": "ตรวจพบปลั๊กอินใหม่ รีสตาร์ทเกมเพื่อเปิดใช้งาน หรือกำหนดค่าได้ในการตั้งค่า", + "pluginsRemovedText": "ไม่พบปลั๊กอิน ${NUM} รายการอีกต่อไป", "pluginsText": "ปลั๊กอิน", "practiceText": "ฝึกฝน", "pressAnyButtonPlayAgainText": "กดปุ่มใดก็ได้เพื่อเล่นอีกครั้ง...", @@ -1357,6 +1364,7 @@ "tournamentStandingsText": "อันดับการแข่งขัน", "tournamentText": "การแข่งขัน", "tournamentTimeExpiredText": "เวลาการแข่งขันหมดอายุ", + "tournamentsDisabledWorkspaceText": "การแข่งขันจะถูกปิดการใช้งานเมื่อมีการใช้งานพื้นที่ทำงาน \nหากต้องการเปิดใช้งานการแข่งขันอีกครั้ง ให้ปิดใช้งานพื้นที่ทำงานของคุณแล้วเริ่มใหม่", "tournamentsText": "การแข่งขัน", "translations": { "characterNames": { @@ -1840,6 +1848,8 @@ "winsPlayerText": "${NAME} ชนะ!", "winsTeamText": "${NAME} ชนะ!", "winsText": "${NAME} ชนะ!", + "workspaceSyncErrorText": "เกิดข้อผิดพลาดในการซิงค์ ${WORKSPACE} เปิดlogเพื่อดูรายละเอียด", + "workspaceSyncReuseText": "ไม่สามารถซิงค์ ${WORKSPACE} ได้ จึงนำเวอร์ชันที่ซิงค์ก่อนหน้านี้มาใช้", "worldScoresUnavailableText": "ไม่มีคะแนนโลก", "worldsBestScoresText": "คะแนนที่ดีที่สุดในโลก", "worldsBestTimesText": "เวลาที่ดีที่สุดในโลก", diff --git a/dist/ba_data/data/languages/turkish.json b/dist/ba_data/data/languages/turkish.json index 4532347..b91ab74 100644 --- a/dist/ba_data/data/languages/turkish.json +++ b/dist/ba_data/data/languages/turkish.json @@ -498,6 +498,7 @@ "welcome2Text": "Ayrıca benzer aktivitelerden biletler kazanabilirsin.\nBiletler yeni karakterler, haritalar, mini-oyunlar\nve turnuvalara katılmak için kulanılabilir.", "yourPowerRankingText": "Oyuncu Sıralaman:" }, + "copyConfirmText": "Panoya kopyalandı.", "copyOfText": "Kopya ${NAME}", "copyText": "Kopyala", "createEditPlayerText": "", @@ -625,7 +626,7 @@ "epicDescriptionFilterText": "${DESCRIPTION} epik ağırçekim.", "epicNameFilterText": "Epik ${NAME}", "errorAccessDeniedText": "erişim reddedildi", - "errorDeviceTimeIncorrectText": "Eyvah! Cihazın ile sunucu arasındaki zaman farkı ${HOURS} saat.\nBu bazı sorunlara yol açabilir.\nLütfen saat ve saat dilimi ayarlarını kontrol et.", + "errorDeviceTimeIncorrectText": "Cihazın ile sunucu arasındaki zaman farkı ${HOURS} saat.\nBu bazı sorunlara yol açabilir.\nLütfen saat ve saat dilimi ayarlarını kontrol et.", "errorOutOfDiskSpaceText": "disk alanı doldu", "errorSecureConnectionFailText": "Güvenli bulut bağlantısı kurulamadı; ağ işlevi başarısız olabilir.", "errorText": "Hata", @@ -1367,6 +1368,7 @@ "tournamentStandingsText": "Turnuva Kazananları", "tournamentText": "Turnuva", "tournamentTimeExpiredText": "Turnuva Sona Erdi", + "tournamentsDisabledWorkspaceText": "Çalışma alanları aktif olduğunda turnuvalar devre dışı bırakılır.\nTurnuvaları yeniden etkinleştirmek için çalışma alanınızı devre dışı bırakın ve yeniden başlatın.", "tournamentsText": "Turnuvalar", "translations": { "characterNames": { diff --git a/dist/ba_data/data/languages/ukrainian.json b/dist/ba_data/data/languages/ukrainian.json index 009d31a..5f09baa 100644 --- a/dist/ba_data/data/languages/ukrainian.json +++ b/dist/ba_data/data/languages/ukrainian.json @@ -328,6 +328,7 @@ "achievementsRemainingText": "Досягнень залишилось:", "achievementsText": "Досягнення", "achievementsUnavailableForOldSeasonsText": "На жаль, специфіка досягнення недоступна для старих сезонів.", + "activatedText": "${THING} активовано", "addGameWindow": { "getMoreGamesText": "Ще ігри...", "titleText": "Додати гру" @@ -625,7 +626,9 @@ "epicDescriptionFilterText": "${DESCRIPTION} в епічному сповільненій дії.", "epicNameFilterText": "${NAME} в епічному режимі", "errorAccessDeniedText": "доступ заборонено", + "errorDeviceTimeIncorrectText": "Час вашого пристрою зміщено на${HOURS} год.\nЦе може спричинити проблеми.\nБудь ласка, перевірте свій час і налаштування часового поясу.", "errorOutOfDiskSpaceText": "немає місця на диску", + "errorSecureConnectionFailText": "Неможливо встановити безпечне хмарне з’єднання; мережеві функції можуть не працювати.", "errorText": "Помилка", "errorUnknownText": "невідома помилка", "exitGameText": "Вийти з ${APP_NAME}?", @@ -788,7 +791,7 @@ "ticketPack4Text": "Величезна пачка квитків", "ticketPack5Text": "Слонова пачка квитків", "ticketPack6Text": "Максимальна пачка квитків", - "ticketsFromASponsorText": "Отримати ${COUNT} квитків\nвід спонсора", + "ticketsFromASponsorText": "Перегляньте рекламу\nза ${COUNT} квитків", "ticketsText": "Квитків: ${COUNT}", "titleText": "Отримати квитки", "unavailableLinkAccountText": "Вибачте, але на цій платформі покупки недоступні.\nВ якості вирішення, ви можете прив'язати цей акаунт\nдо акаунту на іншій платформі, і здійснювати покупки там.", @@ -799,6 +802,7 @@ "youHaveText": "У вас ${COUNT} квитків" }, "googleMultiplayerDiscontinuedText": "Пробачте, але сервіс мультіплеєра від Google тепер не доступний.\nЯ працюю над зміною сервіса як можно скоріше.\nДо цього, будь ласка, подивіться інакші способи гри в мультіплеєр. \n-Ерік", + "googlePlayPurchasesNotAvailableText": "Покупки в Google Play недоступні.\nМожливо, вам знадобиться оновити програму магазину.", "googlePlayText": "Google Play", "graphicsSettingsWindow": { "alwaysText": "Завжди", @@ -1109,7 +1113,10 @@ "playlistsText": "Плейлисти", "pleaseRateText": "Якщо вам подобається гра ${APP_NAME}, будь ласка, подумайте про те,\nщоб оцінити її або написати рецензію. Це забезпечує корисну\nзворотний зв'язок і допомагає підтримати подальшу розробку.\n\nДякуємо!\n-Ерік", "pleaseWaitText": "Будь ласка зачекайте...", - "pluginsDetectedText": "Новий плагін(и) виявлені.Ввімкніть/підтвердіть це в налаштуваннях.", + "pluginClassLoadErrorText": "Помилка завантаження класу плагіна \"${PLUGIN}\": ${ERROR}", + "pluginInitErrorText": "Помилка запуску плагіна \"${PLUGIN}\": ${ERROR}", + "pluginsDetectedText": "Виявлено нові плагіни. Перезапустіть, щоб активувати їх, або налаштуйте їх у налаштуваннях", + "pluginsRemovedText": "${NUM} плагін(ів) більше не знайдено.", "pluginsText": "Плагіни", "practiceText": "Тренування", "pressAnyButtonPlayAgainText": "Натисніть будь-яку кнопку щоб грати знову...", @@ -1360,6 +1367,7 @@ "tournamentStandingsText": "Позиції в турнірі", "tournamentText": "Турнір", "tournamentTimeExpiredText": "Час турніру минув", + "tournamentsDisabledWorkspaceText": "Турніри вимкнені, коли робочі області активні.\n Щоб знову ввімкнути турніри, вимкніть робочу область і перезапустіть.", "tournamentsText": "Турніри", "translations": { "characterNames": { @@ -1845,6 +1853,8 @@ "winsPlayerText": "Переміг ${NAME}!", "winsTeamText": "Перемогли ${NAME}!", "winsText": "${NAME} виграв!", + "workspaceSyncErrorText": "Помилка синхронізації ${WORKSPACE}. Подробиці дивіться в журналі.", + "workspaceSyncReuseText": "Не вдається синхронізувати ${WORKSPACE}. Повторне використання попередньої синхронізованої версії.", "worldScoresUnavailableText": "Світові результати недоступні.", "worldsBestScoresText": "Кращі в світі результати", "worldsBestTimesText": "Кращий світовий час", diff --git a/dist/ba_data/data/languages/venetian.json b/dist/ba_data/data/languages/venetian.json index 2c942e2..7813a92 100644 --- a/dist/ba_data/data/languages/venetian.json +++ b/dist/ba_data/data/languages/venetian.json @@ -4,7 +4,7 @@ "accountsText": "Account", "achievementProgressText": "Obietivi: ${COUNT} de ${TOTAL}", "campaignProgressText": "Progreso canpagna [Defìsiłe]: ${PROGRESS}", - "changeOncePerSeason": "A te połi canbiar ’sto dato soło na volta par stajon.", + "changeOncePerSeason": "Te połi canbiar ’sto dato soło na volta par stajon.", "changeOncePerSeasonError": "Par canbiarlo te ghè da spetar ła pròsema stajon (${NUM} days).", "customName": "Nome parsonałizà", "linkAccountsEnterCodeText": "Insarisi còdaze", @@ -18,7 +18,7 @@ "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?", "resetProgressText": "Ełìmena progresi", "setAccountName": "Inposta un nome utente", - "setAccountNameDesc": "Sełesiona el nome da vizuałizar so’l to account.\nA te połi doparar el nome da uno de i to account\ncołegài o crear un nome parsonałizà ma ùnivogo.", + "setAccountNameDesc": "Sełesiona el nome da vizuałizar so’l to account.\nTe połi doparar el nome da uno de i to account\ncołegài o crear un nome parsonałizà ma ùnivogo.", "signInInfoText": "Conétate par tirar sù biłieti, batajar online e\nsparpagnar i to progresi infrà dispozidivi defarenti.", "signInText": "Conétate", "signInWithDeviceInfoText": "(par 'sto dispozidivo ze disponìbiłe un soło account automàtego)", @@ -333,12 +333,12 @@ "alreadySignedInText": "El to account el ze in dòparo inte n’antro\ndispozidivo: canbia account o sara sù el zugo\ninte cheł’altro to dispozidivo e proa danovo.", "apiVersionErrorText": "Inposìbiłe cargar el mòduło ${NAME}, el se refarise a ła varsion ${VERSION_USED}. Serve invese ła ${VERSION_REQUIRED}.", "audioSettingsWindow": { - "headRelativeVRAudioInfoText": "(Ativa \"Auto\" soło co A te tachi sù łe fonarołe par ła realtà virtuałe)", + "headRelativeVRAudioInfoText": "(Ativa \"Auto\" soło co te tachi sù łe fonarołe par ła realtà virtuałe)", "headRelativeVRAudioText": "Àudio par fonarołe VR", "musicVolumeText": "Vołume mùzega", "soundVolumeText": "Vołume son", "soundtrackButtonText": "Son de fondo", - "soundtrackDescriptionText": "(scolta ła to mùzega fin che A te zughi)", + "soundtrackDescriptionText": "(scolta ła to mùzega fin che te zughi)", "titleText": "Àudio" }, "autoText": "Automàtega", @@ -376,7 +376,7 @@ }, "configGamepadSelectWindow": { "androidNoteText": "Nota: ła conpatibiłidà par i controładori ła muda drio dispozidivo e varsion de Android.", - "pressAnyButtonText": "Struca un boton calsìase de'l controłador\nche A te vołi configurar...", + "pressAnyButtonText": "Struca un boton calsìase de'l controłador\nche te vołi configurar...", "titleText": "Configura controładori" }, "configGamepadWindow": { @@ -459,7 +459,7 @@ "controlsText": "Comandi", "coopSelectWindow": { "activenessAllTimeInfoText": "'Sto chive no'l vien aplegà inte ła clasìfega globałe.", - "activenessInfoText": "'Sto moltiplegador el và sù inte i dì co\nA te zughi e el và zó inte cheł'altri.", + "activenessInfoText": "'Sto moltiplegador el và sù inte i dì co\nte zughi e el và zó inte cheł'altri.", "activityText": "Costansa", "campaignText": "Canpagna", "challengesInfoText": "Vadagna i premi conpletando i minizughi.\n\nI premi e ła defegoltà de i łevełi i ndarà\nsù par cauna sfida conpletada e i ndarà zó\nco łe vien perdeste o miga zugàe.", @@ -492,9 +492,10 @@ "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.", "welcome1Text": "Benrivài inte ła ${LEAGUE}. A te połi mejorar ła to\npozision vadagnando stełe inte i łevełi, conpletando\ni obietivi o vinsendo i trofèi inte i tornèi.", - "welcome2Text": "Fazendo racuante atividà de 'sto tipo A te połi anca vadagnar biłieti.\nI biłieti i połe èsar doparài par dezblocar parsonaji novi, łevełi e\nminizughi ma anca par ndar rento a tornèi o ver funsion in pì.", + "welcome2Text": "Fazendo racuante atividà de 'sto tipo te połi anca vadagnar biłieti.\nI biłieti i połe èsar doparài par dezblocar parsonaji novi, łevełi e\nminizughi ma anca par ndar rento a tornèi o ver funsion in pì.", "yourPowerRankingText": "Ła to pozision:" }, + "copyConfirmText": "Copià inte łe note.", "copyOfText": "Copia de ${NAME}", "copyText": "Copia", "createEditPlayerText": "", @@ -503,7 +504,7 @@ "additionalAudioArtIdeasText": "Soni adisionałi, gràfega inisiałe e idee de ${NAME}", "additionalMusicFromText": "Mùzega adisionałe de ${NAME}", "allMyFamilyText": "ła me fameja e tuti i me amighi che i gà jutà a testar el zugo", - "codingGraphicsAudioText": "Tradusion in łengua veneta: Còdaze Veneto\n Mail: codazeveneto@gmail.com - Telegram: @LenguaVeneta\n\nProgramasion, gràfega e àudio de ${NAME}", + "codingGraphicsAudioText": "Tradusion in łengua veneta: VeC - Łengua Veneta\n Mail: venetianlanguage@gmail.com - Telegram: @LenguaVeneta\n\nProgramasion, gràfega e àudio de ${NAME}", "languageTranslationsText": "Tradusion inte cheł'altre łengue:", "legalText": "Informasion łegałi:", "publicDomainMusicViaText": "Mùzega a dòparo pùblego de ${NAME}", @@ -586,7 +587,7 @@ "titleEditText": "Muda profiło", "titleNewText": "Profiło novo", "unavailableText": "\"${NAME}\" no'l ze miga disponìbiłe: proa n'antro nome.", - "upgradeProfileInfoText": "Ndando vanti A te reservarè el to nome zugador in tuto\nel mondo e te podarè zontarghe na icona parsonałizada.", + "upgradeProfileInfoText": "Ndando vanti te reservarè el to nome zugador in tuto\nel mondo e te podarè zontarghe na icona parsonałizada.", "upgradeToGlobalProfileText": "Mejora a profiło globałe" }, "editSoundtrackWindow": { @@ -622,7 +623,9 @@ "epicDescriptionFilterText": "${DESCRIPTION} in movensa camoma.", "epicNameFilterText": "${NAME} in movensa camoma", "errorAccessDeniedText": "aceso refudà", + "errorDeviceTimeIncorrectText": "L'ora de'l to dispozidivo ła ze zbałada de ${HOURS} ore.\nPodarìa verifegarse problemi.\nControła l'ora e łe inpostasion de'l to fuzorario.", "errorOutOfDiskSpaceText": "spasio so'l disco fenìo", + "errorSecureConnectionFailText": "Inposìbiłe stabiłir na conesion segura co ła nùvoła: podarìa èsarghe erori co łe funsionałidà de rede.", "errorText": "Eror", "errorUnknownText": "eror miga conosesto", "exitGameText": "Vutu ndar fora da ${APP_NAME}?", @@ -686,7 +689,7 @@ "copyCodeText": "Copia còdaze", "dedicatedServerInfoText": "Inposta un server dedegà par rezultài pì boni. Daghe un ocio so bombsquadgame.com/server par capir come far.", "disconnectClientsText": "'Sta oparasion ła desconetarà ${COUNT} zugador/i\nda'l to grupo. Vutu ndar vanti?", - "earnTicketsForRecommendingAmountText": "Se i provarà el zugo, i to amighi i resevarà ${COUNT} biłieti\n(e ti A te ghin resevarè ${YOU_COUNT} par caun de łori che'l ło dopararà)", + "earnTicketsForRecommendingAmountText": "Se i provarà el zugo, i to amighi i resevarà ${COUNT} biłieti\n(e ti te ghin resevarè ${YOU_COUNT} par caun de łori che'l ło dopararà)", "earnTicketsForRecommendingText": "Sparpagna el zugo par\nver biłieti gratùidi...", "emailItText": "Màndeło par mail", "favoritesSaveText": "Salva inte i prefarìì", @@ -695,7 +698,7 @@ "freeCloudServerAvailableNowText": "Disponìbiłe server agratis!", "freeCloudServerNotAvailableText": "Gnaun server agratis disponìbiłe.", "friendHasSentPromoCodeText": "Par ti ${COUNT} biłieti de ${APP_NAME} da ${NAME}!", - "friendPromoCodeAwardText": "Tute łe 'olte che'l vegnarà doparà ti A te resevarè ${COUNT} biłieti.", + "friendPromoCodeAwardText": "Tute łe 'olte che'l vegnarà doparà te resevarè ${COUNT} biłieti.", "friendPromoCodeExpireText": "El còdaze el ze soło par i zugaduri novi e el terminarà tenpo ${EXPIRE_HOURS} ore.", "friendPromoCodeInstructionsText": "Par dopararlo, verzi ${APP_NAME} e và so \"Inpostasion > Avansàe > Insarisi còdaze\".\nDaghe un ocio so bombsquadgame.com par i link de descargamento de'l zugo par tute łe piataforme conpatìbiłi.", "friendPromoCodeRedeemLongText": "El połe èsar scanbià par ${COUNT} biłieti gratùidi da ${MAX_USES} parsone.", @@ -755,7 +758,7 @@ "privateText": "Privà", "publicHostRouterConfigText": "Podarìa servir configurar na porta spesìfega so'l to router. Ospidar un grupo privà ze pì fàsiłe.", "publicText": "Pùblego", - "requestingAPromoCodeText": "Drio far domanda de un còdaze...", + "requestingAPromoCodeText": "Dimanda par un còdaze…", "sendDirectInvitesText": "Manda invidi direti", "shareThisCodeWithFriendsText": "Sparpagna 'sto còdaze co i amighi:", "showMyAddressText": "Mostra el me ndariso", @@ -776,19 +779,19 @@ "freeText": "GRATIS!", "freeTicketsText": "Biłieti gratùidi", "inProgressText": "Na tranzasion ła ze dezà in ełaborasion: proa danovo infrà na scianta.", - "purchasesRestoredText": "Cronpade repristenàe.", + "purchasesRestoredText": "Cronpe recuparàe.", "receivedTicketsText": "${COUNT} biłieti resevesti!", - "restorePurchasesText": "Reprìstena cronpade", + "restorePurchasesText": "Recùpara cronpe.", "ticketPack1Text": "Pacheto de biłieti ceło", "ticketPack2Text": "Pacheto de biłieti mezan", "ticketPack3Text": "Pacheto de biłieti grando", "ticketPack4Text": "Pacheto de biłieti ultra", "ticketPack5Text": "Pacheto de biłieti despropozità", "ticketPack6Text": "Pacheto de biłieti defenidivo", - "ticketsFromASponsorText": "Vadagna ${COUNT} biłieti\nco na reclan", + "ticketsFromASponsorText": "Varda na reclan e\notien ${COUNT} biłieti", "ticketsText": "${COUNT} biłieti", "titleText": "Otien biłieti", - "unavailableLinkAccountText": "Ne despiaze, A no se połe miga cronpar so 'sta piataforma.\nVołendo, cofà sołusion, A te połi cołegar 'sto account co\nuno inte n'antra piataforma e cronpar calcosa da łà.", + "unavailableLinkAccountText": "No se połe miga cronpar so 'sta piataforma.\nVołendo, te połi cołegar 'sto account co uno inte\nn'antra piataforma e cronpar calcosa da łà.", "unavailableTemporarilyText": "'Sta funsion no ła ze miga disponìbiłe par deso: proa danovo pì tardi.", "unavailableText": "Ne despiaze, 'sta funsion no ła ze miga disponìbiłe.", "versionTooOldText": "Ne despiaze, 'sta varsion ła ze masa vecia: ajorna el zugo co cheła nova.", @@ -796,6 +799,7 @@ "youHaveText": "A te ghè ${COUNT} biłieti" }, "googleMultiplayerDiscontinuedText": "Me despiaze, el sarviso multizugador de Google no'l ze miga pì disponìbiłe.\nA sò drio łaorar a un renpiaso pì in presa che se połe.\nIntanto proa n'antro mètodo de conesion.\n-Eric", + "googlePlayPurchasesNotAvailableText": "Łe cronpe vecie no łe ze miga disponìbiłi.\nPodarìa èsarghe bezogno de ajornar l'apl Google Play.", "googlePlayText": "Google Play", "graphicsSettingsWindow": { "alwaysText": "Senpre", @@ -1295,7 +1299,7 @@ "charactersText": "Parsonaji", "comingSoonText": "E presto...", "extrasText": "Extra", - "freeBombSquadProText": "Daromài BombSquad el ze gratis, ma visto che A te ło ghivi dezà cronpà prima,\nA te vegnarà dezblocà el mejoramento BombSquad Pro e A te vegnarà zontài ${COUNT}\nbiłieti cofà rengrasiamento. Gòdate łe funsion nove, e grasie de'l to suporto!\n-Eric", + "freeBombSquadProText": "Daromài BombSquad el ze gratis, ma visto che te ło ghivi dezà cronpà prima,\nte vegnarà dezblocà el mejoramento BombSquad Pro e te vegnarà zontài ${COUNT}\nbiłieti cofà rengrasiamento. Gòdate łe funsion nove, e grasie de'l to suporto!\n-Eric", "holidaySpecialText": "Spesiałe feste", "howToSwitchCharactersText": "(và so \"${SETTINGS} > ${PLAYER_PROFILES}\" par sernir i to parsonaji e parsonałizarli)", "howToUseIconsText": "(par dopararle, crea un profiło zugador globałe inte ła sesion account)", @@ -1360,6 +1364,7 @@ "tournamentStandingsText": "Clasìfega tornèo", "tournamentText": "Tornèo", "tournamentTimeExpiredText": "Tenpo de'l tornèo fenìo!", + "tournamentsDisabledWorkspaceText": "Co te ghè modifegasion ative i tornèi i vien dezativài.\nPar ativarli danovo, dezativa łe modifegasion e retaca l'apl.", "tournamentsText": "Tornèi", "translations": { "characterNames": { @@ -1851,7 +1856,7 @@ "xbox360ControllersWindow": { "getDriverText": "Descarga el driver", "macInstructions2Text": "Par doparar sensa fiło i controładori, A te serve anca el resevidor\nche'l riva co l''Xbox 360 Wireless Controller par Windows'.\nUn resevidor el te parmete de conétar fin a 4 controładori.\n\nInportante: i resevidori de terse parti no i funsionarà miga co 'sto driver;\nsegùrate che'l to resevidor el sipia 'Microsoft' e miga 'XBOX 360'.\nŁa Microsoft no łi vende pì destacài, donca te servirà par forsa cheło\nvendesto insebre co'l controłador, o senò, proa sercar so Ebay.\n\nSe te cati ùtiłe el driver, ciapa in considerasion de farghe na\ndonasion a'l só dezviłupador so 'sto sito.", - "macInstructionsText": "Per doparar i controładori co'l fiło de ła Xbox 360, A te ghè\nda instałar el driver Mac disponìbiłe so'l link cuà soto.\nEl funsiona co anbo i controładori, co'l fiło o sensa.", + "macInstructionsText": "Per doparar i controładori co'l fiło de ła Xbox 360, te ghè\nda instałar el driver Mac disponìbiłe so'l link cuà soto.\nEl funsiona co anbo i controładori, co'l fiło o sensa.", "ouyaInstructionsText": "Par doparar so Bombsquad un controłador de l'Xbox 360 co'l fiło,\ntàcheło sù inte ła porta USB de’l to dispozidivo. Te połi anca\ntacar sù pì controładori insenbre doparando un hub USB.\n\nPar doparar i controładori sensa fiło invese, te serve un resevidor\nde segnałe. Te połi catarlo, o rento ła scàtoła \"Controładori sensa fiło\nXbox 360 par Windows\", o vendesto a parte. Caun resevidor el và tacà so\nna porta USB e el te parmete de conétar fin a 4 controładori.", "titleText": "Doparar un controłador Xbox 360 co ${APP_NAME}:" }, diff --git a/dist/ba_data/data/languages/vietnamese.json b/dist/ba_data/data/languages/vietnamese.json index 92fd2af..5949d4b 100644 --- a/dist/ba_data/data/languages/vietnamese.json +++ b/dist/ba_data/data/languages/vietnamese.json @@ -329,6 +329,7 @@ "achievementsRemainingText": "Các thành tựu tiếp theo:", "achievementsText": "Các thành tựu", "achievementsUnavailableForOldSeasonsText": "Xin lỗi , huy hiệu này không có ở mùa trước", + "activatedText": "${THING} đã được kích hoạt.", "addGameWindow": { "getMoreGamesText": "Thêm các thể loại chơi", "titleText": "Thêm trận đấu" @@ -627,7 +628,9 @@ "epicDescriptionFilterText": "${DESCRIPTION} trong chế độ quay chậm.", "epicNameFilterText": "${NAME} Quay Chậm", "errorAccessDeniedText": "từ chối kết nối", + "errorDeviceTimeIncorrectText": "Thời gian thiết bị của bạn tắt ${HOURS} giờ.\nĐiều này có thể gây ra vấn đề.\nVui lòng kiểm tra cài đặt múi giờ và múi giờ của bạn.", "errorOutOfDiskSpaceText": "hết bộ nhớ", + "errorSecureConnectionFailText": "Không thể thiết lập kết nối đám mây an toàn; chức năng mạng có thể bị lỗi.", "errorText": "Lỗi", "errorUnknownText": "Không rõ lỗi", "exitGameText": "Thoát ${APP_NAME}?", @@ -790,7 +793,7 @@ "ticketPack4Text": "Gói vé siêu lớn", "ticketPack5Text": "Gói vé khổng lồ", "ticketPack6Text": "Gói vé siêu khổng lồ", - "ticketsFromASponsorText": "Lấy ${COUNT} vé \ntừ một nhà tài trợ", + "ticketsFromASponsorText": "Xem một quảng cáo\ncho ${COUNT} vé", "ticketsText": "${COUNT} Vé", "titleText": "Lấy vé", "unavailableLinkAccountText": "Xin lỗi, không thể mua hàng trên hệ điều hành này.\nBạn có thể, liên kết tài khoản này\ntới một tài khoản trên hệ điều hành khác và mua hàng ở đó.", @@ -801,6 +804,7 @@ "youHaveText": "Bạn có ${COUNT} vé" }, "googleMultiplayerDiscontinuedText": "Xin lỗi, dịch vụ Google nhiều người chơi không còn nữa.\nTôi đang cố gắng thay thế nhanh nhất có thể.\nTrong lúc đó, vui lòng thử cách kết nối khác.\n-Eric", + "googlePlayPurchasesNotAvailableText": "Các giao dịch mua trên Google Play không khả dụng.\nBạn có thể cần cập nhật ứng dụng cửa hàng của mình.", "googlePlayText": "Google Trò chơi", "graphicsSettingsWindow": { "alwaysText": "Luôn Luôn", @@ -1111,7 +1115,10 @@ "playlistsText": "Danh sách", "pleaseRateText": "Nếu bạn thấy ${APP_NAME} vui lòng \nđánh giá hoặc viết \ncảm nhận.\nĐiều này giúp hỗ trợ phát triển trong tương lai.\ncảm ơn!\n-eric", "pleaseWaitText": "Vui lòng chờ...", - "pluginsDetectedText": "Đã phát hiện plugin mới. Kích hoạt / cấu hình chúng trong cài đặt.", + "pluginClassLoadErrorText": "Lỗi khi tải lớp plugin '${PLUGIN}': ${ERROR}", + "pluginInitErrorText": "Lỗi khi nhập plugin '${PLUGIN}': ${ERROR}", + "pluginsDetectedText": "Đã phát hiện (các) plugin mới. Khởi động lại để kích hoạt chúng hoặc định cấu hình chúng trong cài đặt.", + "pluginsRemovedText": "Không còn tìm thấy ${NUM} plugin nào nữa.", "pluginsText": "Cắm", "practiceText": "Luyện tập", "pressAnyButtonPlayAgainText": "Nhấn nút bất kỳ để chơi lại...", @@ -1362,6 +1369,7 @@ "tournamentStandingsText": "Bảng xếp hạng giải đấu", "tournamentText": "Giải đấu", "tournamentTimeExpiredText": "Giải đấu đã hết thời gian.", + "tournamentsDisabledWorkspaceText": "Các giải đấu bị vô hiệu hóa khi không gian làm việc đang hoạt động.\nĐể bật lại các giải đấu, hãy tắt không gian làm việc của bạn và khởi động lại.", "tournamentsText": "Giải đấu", "translations": { "characterNames": { @@ -1845,6 +1853,8 @@ "winsPlayerText": "${NAME} Chiến thắng!", "winsTeamText": "${NAME} Chiến thắng!", "winsText": "${NAME} Chiến thắng!", + "workspaceSyncErrorText": "Lỗi đồng bộ hóa ${WORKSPACE}. Xem nhật ký để biết chi tiết.", + "workspaceSyncReuseText": "Không thể đồng bộ hóa ${WORKSPACE}. Sử dụng lại phiên bản đã đồng bộ hóa trước đó.", "worldScoresUnavailableText": "Điểm trên thế giới không có sẵn.", "worldsBestScoresText": "Điểm số thế giới cao nhất", "worldsBestTimesText": "Thời gian tốt nhất thế giới", diff --git a/dist/ba_data/python-site-packages/certifi/__init__.py b/dist/ba_data/python-site-packages/certifi/__init__.py index bdeb06b..6257533 100644 --- a/dist/ba_data/python-site-packages/certifi/__init__.py +++ b/dist/ba_data/python-site-packages/certifi/__init__.py @@ -1,4 +1,4 @@ from .core import contents, where __all__ = ["contents", "where"] -__version__ = "2022.06.15" +__version__ = "2022.09.14" diff --git a/dist/ba_data/python-site-packages/certifi/cacert.pem b/dist/ba_data/python-site-packages/certifi/cacert.pem index ee9be4c..f253132 100644 --- a/dist/ba_data/python-site-packages/certifi/cacert.pem +++ b/dist/ba_data/python-site-packages/certifi/cacert.pem @@ -4683,3 +4683,65 @@ ADBmAjEA5gVYaWHlLcoNy/EZCL3W/VGSGn5jVASQkZo1kTmZ+gepZpO6yGjUij/6 7W4WAie3AjEA3VoXK3YdZUKWpqxdinlW2Iob35reX8dQj7FbcQwm32pAAOwzkSFx vmjkI6TZraE3 -----END CERTIFICATE----- + +# Issuer: CN=Security Communication RootCA3 O=SECOM Trust Systems CO.,LTD. +# Subject: CN=Security Communication RootCA3 O=SECOM Trust Systems CO.,LTD. +# Label: "Security Communication RootCA3" +# Serial: 16247922307909811815 +# MD5 Fingerprint: 1c:9a:16:ff:9e:5c:e0:4d:8a:14:01:f4:35:5d:29:26 +# SHA1 Fingerprint: c3:03:c8:22:74:92:e5:61:a2:9c:5f:79:91:2b:1e:44:13:91:30:3a +# SHA256 Fingerprint: 24:a5:5c:2a:b0:51:44:2d:06:17:76:65:41:23:9a:4a:d0:32:d7:c5:51:75:aa:34:ff:de:2f:bc:4f:5c:52:94 +-----BEGIN CERTIFICATE----- +MIIFfzCCA2egAwIBAgIJAOF8N0D9G/5nMA0GCSqGSIb3DQEBDAUAMF0xCzAJBgNV +BAYTAkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMScw +JQYDVQQDEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTMwHhcNMTYwNjE2 +MDYxNzE2WhcNMzgwMTE4MDYxNzE2WjBdMQswCQYDVQQGEwJKUDElMCMGA1UEChMc +U0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UEAxMeU2VjdXJpdHkg +Q29tbXVuaWNhdGlvbiBSb290Q0EzMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEA48lySfcw3gl8qUCBWNO0Ot26YQ+TUG5pPDXC7ltzkBtnTCHsXzW7OT4r +CmDvu20rhvtxosis5FaU+cmvsXLUIKx00rgVrVH+hXShuRD+BYD5UpOzQD11EKzA +lrenfna84xtSGc4RHwsENPXY9Wk8d/Nk9A2qhd7gCVAEF5aEt8iKvE1y/By7z/MG +TfmfZPd+pmaGNXHIEYBMwXFAWB6+oHP2/D5Q4eAvJj1+XCO1eXDe+uDRpdYMQXF7 +9+qMHIjH7Iv10S9VlkZ8WjtYO/u62C21Jdp6Ts9EriGmnpjKIG58u4iFW/vAEGK7 +8vknR+/RiTlDxN/e4UG/VHMgly1s2vPUB6PmudhvrvyMGS7TZ2crldtYXLVqAvO4 +g160a75BflcJdURQVc1aEWEhCmHCqYj9E7wtiS/NYeCVvsq1e+F7NGcLH7YMx3we +GVPKp7FKFSBWFHA9K4IsD50VHUeAR/94mQ4xr28+j+2GaR57GIgUssL8gjMunEst ++3A7caoreyYn8xrC3PsXuKHqy6C0rtOUfnrQq8PsOC0RLoi/1D+tEjtCrI8Cbn3M +0V9hvqG8OmpI6iZVIhZdXw3/JzOfGAN0iltSIEdrRU0id4xVJ/CvHozJgyJUt5rQ +T9nO/NkuHJYosQLTA70lUhw0Zk8jq/R3gpYd0VcwCBEF/VfR2ccCAwEAAaNCMEAw +HQYDVR0OBBYEFGQUfPxYchamCik0FW8qy7z8r6irMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBDAUAA4ICAQDcAiMI4u8hOscNtybS +YpOnpSNyByCCYN8Y11StaSWSntkUz5m5UoHPrmyKO1o5yGwBQ8IibQLwYs1OY0PA +FNr0Y/Dq9HHuTofjcan0yVflLl8cebsjqodEV+m9NU1Bu0soo5iyG9kLFwfl9+qd +9XbXv8S2gVj/yP9kaWJ5rW4OH3/uHWnlt3Jxs/6lATWUVCvAUm2PVcTJ0rjLyjQI +UYWg9by0F1jqClx6vWPGOi//lkkZhOpn2ASxYfQAW0q3nHE3GYV5v4GwxxMOdnE+ +OoAGrgYWp421wsTL/0ClXI2lyTrtcoHKXJg80jQDdwj98ClZXSEIx2C/pHF7uNke +gr4Jr2VvKKu/S7XuPghHJ6APbw+LP6yVGPO5DtxnVW5inkYO0QR4ynKudtml+LLf +iAlhi+8kTtFZP1rUPcmTPCtk9YENFpb3ksP+MW/oKjJ0DvRMmEoYDjBU1cXrvMUV +nuiZIesnKwkK2/HmcBhWuwzkvvnoEKQTkrgc4NtnHVMDpCKn3F2SEDzq//wbEBrD +2NCcnWXL0CsnMQMeNuE9dnUM/0Umud1RvCPHX9jYhxBAEg09ODfnRDwYwFMJZI// +1ZqmfHAuc1Uh6N//g7kdPjIe1qZ9LPFm6Vwdp6POXiUyK+OVrCoHzrQoeIY8Laad +TdJ0MN1kURXbg4NR16/9M51NZg== +-----END CERTIFICATE----- + +# Issuer: CN=Security Communication ECC RootCA1 O=SECOM Trust Systems CO.,LTD. +# Subject: CN=Security Communication ECC RootCA1 O=SECOM Trust Systems CO.,LTD. +# Label: "Security Communication ECC RootCA1" +# Serial: 15446673492073852651 +# MD5 Fingerprint: 7e:43:b0:92:68:ec:05:43:4c:98:ab:5d:35:2e:7e:86 +# SHA1 Fingerprint: b8:0e:26:a9:bf:d2:b2:3b:c0:ef:46:c9:ba:c7:bb:f6:1d:0d:41:41 +# SHA256 Fingerprint: e7:4f:bd:a5:5b:d5:64:c4:73:a3:6b:44:1a:a7:99:c8:a6:8e:07:74:40:e8:28:8b:9f:a1:e5:0e:4b:ba:ca:11 +-----BEGIN CERTIFICATE----- +MIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYT +AkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYD +VQQDEyJTZWN1cml0eSBDb21tdW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYx +NjA1MTUyOFoXDTM4MDExODA1MTUyOFowYTELMAkGA1UEBhMCSlAxJTAjBgNVBAoT +HFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKzApBgNVBAMTIlNlY3VyaXR5 +IENvbW11bmljYXRpb24gRUNDIFJvb3RDQTEwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AASkpW9gAwPDvTH00xecK4R1rOX9PVdu12O/5gSJko6BnOPpR27KkBLIE+Cnnfdl +dB9sELLo5OnvbYUymUSxXv3MdhDYW72ixvnWQuRXdtyQwjWpS4g8EkdtXP9JTxpK +ULGjQjBAMB0GA1UdDgQWBBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu +9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3LsnNdo4gIxwwCMQDAqy0O +be0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70eN9k= +-----END CERTIFICATE----- diff --git a/dist/ba_data/python-site-packages/certifi/core.py b/dist/ba_data/python-site-packages/certifi/core.py index 497d938..de02898 100644 --- a/dist/ba_data/python-site-packages/certifi/core.py +++ b/dist/ba_data/python-site-packages/certifi/core.py @@ -4,12 +4,12 @@ certifi.py This module returns the installation location of cacert.pem or its contents. """ -import os -import types -from typing import Union +import sys -try: - from importlib.resources import path as get_path, read_text + +if sys.version_info >= (3, 11): + + from importlib.resources import as_file, files _CACERT_CTX = None _CACERT_PATH = None @@ -33,13 +33,54 @@ try: # We also have to hold onto the actual context manager, because # it will do the cleanup whenever it gets garbage collected, so # we will also store that at the global level as well. + _CACERT_CTX = as_file(files("certifi").joinpath("cacert.pem")) + _CACERT_PATH = str(_CACERT_CTX.__enter__()) + + return _CACERT_PATH + + def contents() -> str: + return files("certifi").joinpath("cacert.pem").read_text(encoding="ascii") + +elif sys.version_info >= (3, 7): + + from importlib.resources import path as get_path, read_text + + _CACERT_CTX = None + _CACERT_PATH = None + + def where() -> str: + # This is slightly terrible, but we want to delay extracting the + # file in cases where we're inside of a zipimport situation until + # someone actually calls where(), but we don't want to re-extract + # the file on every call of where(), so we'll do it once then store + # it in a global variable. + global _CACERT_CTX + global _CACERT_PATH + if _CACERT_PATH is None: + # This is slightly janky, the importlib.resources API wants you + # to manage the cleanup of this file, so it doesn't actually + # return a path, it returns a context manager that will give + # you the path when you enter it and will do any cleanup when + # you leave it. In the common case of not needing a temporary + # file, it will just return the file system location and the + # __exit__() is a no-op. + # + # We also have to hold onto the actual context manager, because + # it will do the cleanup whenever it gets garbage collected, so + # we will also store that at the global level as well. _CACERT_CTX = get_path("certifi", "cacert.pem") _CACERT_PATH = str(_CACERT_CTX.__enter__()) return _CACERT_PATH + def contents() -> str: + return read_text("certifi", "cacert.pem", encoding="ascii") + +else: + import os + import types + from typing import Union -except ImportError: Package = Union[types.ModuleType, str] Resource = Union[str, "os.PathLike"] @@ -63,6 +104,5 @@ except ImportError: return os.path.join(f, "cacert.pem") - -def contents() -> str: - return read_text("certifi", "cacert.pem", encoding="ascii") + def contents() -> str: + return read_text("certifi", "cacert.pem", encoding="ascii") diff --git a/dist/ba_data/python/_bainternal.py b/dist/ba_data/python/_bainternal.py new file mode 100644 index 0000000..cf89a39 --- /dev/null +++ b/dist/ba_data/python/_bainternal.py @@ -0,0 +1,241 @@ +# Released under the MIT License. See LICENSE for details. +# +"""A dummy stub module for the real _bainternal. + +The real _bainternal is a compiled extension module and only available +in the live engine. This dummy-module allows Pylint/Mypy/etc. to +function reasonably well outside of that environment. + +Make sure this file is never included in dirs seen by the engine! + +In the future perhaps this can be a stub (.pyi) file, but we will need +to make sure that it works with all our tools (mypy, pylint, pycharm). + +NOTE: This file was autogenerated by batools.dummymodule; do not edit by hand. +""" + +# I'm sorry Pylint. I know this file saddens you. Be strong. +# pylint: disable=useless-suppression +# pylint: disable=unnecessary-pass +# pylint: disable=use-dict-literal +# pylint: disable=use-list-literal +# pylint: disable=unused-argument +# pylint: disable=missing-docstring +# pylint: disable=too-many-locals +# pylint: disable=redefined-builtin +# pylint: disable=too-many-lines +# pylint: disable=redefined-outer-name +# pylint: disable=invalid-name +# pylint: disable=no-value-for-parameter + +from __future__ import annotations + +from typing import TYPE_CHECKING, TypeVar + +if TYPE_CHECKING: + from typing import Any, Callable + +_T = TypeVar('_T') + + +def _uninferrable() -> Any: + """Get an "Any" in mypy and "uninferrable" in Pylint.""" + # pylint: disable=undefined-variable + return _not_a_real_variable # type: ignore + + +def add_transaction(transaction: dict, + callback: Callable | None = None) -> None: + """(internal)""" + return None + + +def game_service_has_leaderboard(game: str, config: str) -> bool: + """(internal) + + Given a game and config string, returns whether there is a leaderboard + for it on the game service. + """ + return bool() + + +def get_master_server_address(source: int = -1, version: int = 1) -> str: + """(internal) + + Return the address of the master server. + """ + return str() + + +def get_news_show() -> str: + """(internal)""" + return str() + + +def get_price(item: str) -> str | None: + """(internal)""" + return '' + + +def get_public_login_id() -> str | None: + """(internal)""" + return '' + + +def get_purchased(item: str) -> bool: + """(internal)""" + return bool() + + +def get_purchases_state() -> int: + """(internal)""" + return int() + + +def get_v1_account_display_string(full: bool = True) -> str: + """(internal)""" + return str() + + +def get_v1_account_misc_read_val(name: str, default_value: Any) -> Any: + """(internal)""" + return _uninferrable() + + +def get_v1_account_misc_read_val_2(name: str, default_value: Any) -> Any: + """(internal)""" + return _uninferrable() + + +def get_v1_account_misc_val(name: str, default_value: Any) -> Any: + """(internal)""" + return _uninferrable() + + +def get_v1_account_name() -> str: + """(internal)""" + return str() + + +def get_v1_account_state() -> str: + """(internal)""" + return str() + + +def get_v1_account_state_num() -> int: + """(internal)""" + return int() + + +def get_v1_account_ticket_count() -> int: + """(internal) + + Returns the number of tickets for the current account. + """ + return int() + + +def get_v1_account_type() -> str: + """(internal)""" + return str() + + +def get_v2_fleet() -> str: + """(internal)""" + return str() + + +def have_outstanding_transactions() -> bool: + """(internal)""" + return bool() + + +def in_game_purchase(item: str, price: int) -> None: + """(internal)""" + return None + + +def is_blessed() -> bool: + """(internal)""" + return bool() + + +def mark_config_dirty() -> None: + """(internal) + + Category: General Utility Functions + """ + return None + + +def power_ranking_query(callback: Callable, season: Any = None) -> None: + """(internal)""" + return None + + +def purchase(item: str) -> None: + """(internal)""" + return None + + +def report_achievement(achievement: str, pass_to_account: bool = True) -> None: + """(internal)""" + return None + + +def reset_achievements() -> None: + """(internal)""" + return None + + +def restore_purchases() -> None: + """(internal)""" + return None + + +def run_transactions() -> None: + """(internal)""" + return None + + +def sign_in_v1(account_type: str) -> None: + """(internal) + + Category: General Utility Functions + """ + return None + + +def sign_out_v1(v2_embedded: bool = False) -> None: + """(internal) + + Category: General Utility Functions + """ + return None + + +def submit_score(game: str, + config: str, + name: Any, + score: int | None, + callback: Callable, + 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. + As a courtesy, please don't send fake scores to the server. I'd prefer + to devote my time to improving the game instead of trying to make the + score server more mischief-proof. + """ + return None + + +def tournament_query(callback: Callable[[dict | None], None], + args: dict) -> None: + """(internal)""" + return None diff --git a/dist/ba_data/python/ba/__init__.py b/dist/ba_data/python/ba/__init__.py index 82df942..43d9897 100644 --- a/dist/ba_data/python/ba/__init__.py +++ b/dist/ba_data/python/ba/__init__.py @@ -13,11 +13,11 @@ from _ba import ( Node, SessionPlayer, Sound, Texture, Timer, Vec3, Widget, buttonwidget, camerashake, checkboxwidget, columnwidget, containerwidget, do_once, emitfx, getactivity, getcollidemodel, getmodel, getnodes, getsession, - getsound, gettexture, hscrollwidget, imagewidget, log, newactivity, - newnode, playsound, printnodes, printobjects, pushcall, quit, rowwidget, - safecolor, screenmessage, scrollwidget, set_analytics_screen, charstr, - textwidget, time, timer, open_url, widget, clipboard_is_supported, - clipboard_has_text, clipboard_get_text, clipboard_set_text, getdata) + 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._activity import Activity from ba._plugin import PotentialPlugin, Plugin, PluginSubsystem from ba._actor import Actor @@ -99,10 +99,10 @@ __all__ = [ 'GameTip', 'garbage_collect', 'getactivity', 'getclass', 'getcollidemodel', 'getcollision', 'getdata', 'getmaps', 'getmodel', 'getnodes', 'getsession', 'getsound', 'gettexture', 'HitMessage', 'hscrollwidget', 'imagewidget', - 'ImpactDamageMessage', 'InputDevice', 'InputDeviceNotFoundError', - 'InputType', 'IntChoiceSetting', 'IntSetting', + 'ImpactDamageMessage', 'in_logic_thread', 'InputDevice', + 'InputDeviceNotFoundError', 'InputType', 'IntChoiceSetting', 'IntSetting', 'is_browser_likely_available', 'is_point_in_box', 'Keyboard', - 'LanguageSubsystem', 'Level', 'Lobby', 'log', 'Lstr', 'Map', 'Material', + 'LanguageSubsystem', 'Level', 'Lobby', 'Lstr', 'Map', 'Material', 'MetadataSubsystem', 'Model', 'MultiTeamSession', 'MusicPlayer', 'MusicPlayMode', 'MusicSubsystem', 'MusicType', 'newactivity', 'newnode', 'Node', 'NodeActor', 'NodeNotFoundError', 'normalized_color', diff --git a/dist/ba_data/python/ba/_accountv1.py b/dist/ba_data/python/ba/_accountv1.py index 825a19a..d6cf47d 100644 --- a/dist/ba_data/python/ba/_accountv1.py +++ b/dist/ba_data/python/ba/_accountv1.py @@ -9,6 +9,7 @@ import time from typing import TYPE_CHECKING import _ba +from ba import _internal if TYPE_CHECKING: from typing import Any @@ -41,7 +42,7 @@ class AccountV1Subsystem: def do_auto_sign_in() -> None: if _ba.app.headless_mode or _ba.app.config.get( 'Auto Account State') == 'Local': - _ba.sign_in_v1('Local') + _internal.sign_in_v1('Local') _ba.pushcall(do_auto_sign_in) @@ -108,8 +109,8 @@ class AccountV1Subsystem: if data['p']: pro_mult = 1.0 + float( - _ba.get_v1_account_misc_read_val('proPowerRankingBoost', - 0.0)) * 0.01 + _internal.get_v1_account_misc_read_val('proPowerRankingBoost', + 0.0)) * 0.01 else: pro_mult = 1.0 @@ -135,12 +136,13 @@ class AccountV1Subsystem: """(internal)""" # pylint: disable=cyclic-import from ba import _store - if _ba.get_v1_account_state() != 'signed_in': + 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 _ba.get_purchased(item_name): + if item_name.startswith('icons.') and _internal.get_purchased( + item_name): icons.append(item['icon']) return icons @@ -152,12 +154,13 @@ class AccountV1Subsystem: (internal) """ # This only applies when we're signed in. - if _ba.get_v1_account_state() != 'signed_in': + if _internal.get_v1_account_state() != 'signed_in': return # If the short version of our account name currently cant be # displayed by the game, cancel. - if not _ba.have_chars(_ba.get_v1_account_display_string(full=False)): + if not _ba.have_chars( + _internal.get_v1_account_display_string(full=False)): return config = _ba.app.config @@ -165,7 +168,7 @@ class AccountV1Subsystem: or '__account__' not in config['Player Profiles']): # Create a spaz with a nice default purply color. - _ba.add_transaction({ + _internal.add_transaction({ 'type': 'ADD_PLAYER_PROFILE', 'name': '__account__', 'profile': { @@ -174,7 +177,7 @@ class AccountV1Subsystem: 'highlight': [0.5, 0.25, 1.0] } }) - _ba.run_transactions() + _internal.run_transactions() def have_pro(self) -> bool: """Return whether pro is currently unlocked.""" @@ -182,9 +185,9 @@ class AccountV1Subsystem: # Check our tickets-based pro upgrade and our two real-IAP based # upgrades. Also always unlock this stuff in ballistica-core builds. return bool( - _ba.get_purchased('upgrades.pro') - or _ba.get_purchased('static.pro') - or _ba.get_purchased('static.pro_sale') + _internal.get_purchased('upgrades.pro') + or _internal.get_purchased('static.pro') + or _internal.get_purchased('static.pro_sale') or 'ballistica' + 'core' == _ba.appname()) def have_pro_options(self) -> bool: @@ -199,7 +202,8 @@ class AccountV1Subsystem: # or also if we've been grandfathered in or are using ballistica-core # builds. return self.have_pro() or bool( - _ba.get_v1_account_misc_read_val_2('proOptionsUnlocked', False) + _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: @@ -221,17 +225,17 @@ class AccountV1Subsystem: from ba._language import Lstr # Run any pending promo codes we had queued up while not signed in. - if _ba.get_v1_account_state( + 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)) - _ba.add_transaction({ + _internal.add_transaction({ 'type': 'PROMO_CODE', 'expire_time': time.time() + 5, 'code': code }) - _ba.run_transactions() + _internal.run_transactions() self.pending_promo_codes = [] def add_pending_promo_code(self, code: str) -> None: @@ -242,7 +246,7 @@ class AccountV1Subsystem: # If we're not signed in, queue up the code to run the next time we # are and issue a warning if we haven't signed in within the next # few seconds. - if _ba.get_v1_account_state() != 'signed_in': + if _internal.get_v1_account_state() != 'signed_in': def check_pending_codes() -> None: """(internal)""" @@ -259,9 +263,9 @@ class AccountV1Subsystem: return _ba.screenmessage(Lstr(resource='submittingPromoCodeText'), color=(0, 1, 0)) - _ba.add_transaction({ + _internal.add_transaction({ 'type': 'PROMO_CODE', 'expire_time': time.time() + 5, 'code': code }) - _ba.run_transactions() + _internal.run_transactions() diff --git a/dist/ba_data/python/ba/_achievement.py b/dist/ba_data/python/ba/_achievement.py index 22b1bc5..006e0c7 100644 --- a/dist/ba_data/python/ba/_achievement.py +++ b/dist/ba_data/python/ba/_achievement.py @@ -6,6 +6,7 @@ from __future__ import annotations from typing import TYPE_CHECKING import _ba +from ba import _internal from ba._error import print_exception if TYPE_CHECKING: @@ -317,10 +318,13 @@ class AchievementSubsystem: if not ach.complete: # Report new achievements to the game-service. - _ba.report_achievement(achname) + _internal.report_achievement(achname) # And to our account. - _ba.add_transaction({'type': 'ACHIEVEMENT', 'name': achname}) + _internal.add_transaction({ + 'type': 'ACHIEVEMENT', + 'name': achname + }) # Now attempt to show a banner. self.display_achievement_banner(achname) @@ -409,7 +413,7 @@ def _get_ach_mult(include_pro_bonus: bool = False) -> int: (just for display; changing this here won't affect actual rewards) """ - val: int = _ba.get_v1_account_misc_read_val('achAwardMult', 5) + val: int = _internal.get_v1_account_misc_read_val('achAwardMult', 5) assert isinstance(val, int) if include_pro_bonus and _ba.app.accounts_v1.have_pro(): val *= 2 @@ -496,7 +500,7 @@ class Achievement: # signed in, lets not show them (otherwise we tend to get # confusing 'controller connected' achievements popping up while # waiting to log in which can be confusing). - if _ba.get_v1_account_state() != 'signed_in': + if _internal.get_v1_account_state() != 'signed_in': return # If we're being freshly complete, display/report it and whatnot. @@ -592,8 +596,8 @@ class Achievement: def get_award_ticket_value(self, include_pro_bonus: bool = False) -> int: """Get the ticket award value for this achievement.""" - val: int = (_ba.get_v1_account_misc_read_val('achAward.' + self._name, - self._award) * + val: int = (_internal.get_v1_account_misc_read_val( + 'achAward.' + self._name, self._award) * _get_ach_mult(include_pro_bonus)) assert isinstance(val, int) return val @@ -601,7 +605,7 @@ class Achievement: @property def power_ranking_value(self) -> int: """Get the power-ranking award value for this achievement.""" - val: int = _ba.get_v1_account_misc_read_val( + val: int = _internal.get_v1_account_misc_read_val( 'achLeaguePoints.' + self._name, self._award) assert isinstance(val, int) return val diff --git a/dist/ba_data/python/ba/_actor.py b/dist/ba_data/python/ba/_actor.py index 9be0831..8df85e0 100644 --- a/dist/ba_data/python/ba/_actor.py +++ b/dist/ba_data/python/ba/_actor.py @@ -15,7 +15,7 @@ if TYPE_CHECKING: from typing import Any, Literal import ba -TA = TypeVar('TA', bound='Actor') +ActorT = TypeVar('ActorT', bound='Actor') class Actor: @@ -95,7 +95,7 @@ class Actor: return UNHANDLED - def autoretain(self: TA) -> TA: + def autoretain(self: ActorT) -> ActorT: """Keep this Actor alive without needing to hold a reference to it. This keeps the ba.Actor in existence by storing a reference to it diff --git a/dist/ba_data/python/ba/_ads.py b/dist/ba_data/python/ba/_ads.py index ff688fd..6cf7730 100644 --- a/dist/ba_data/python/ba/_ads.py +++ b/dist/ba_data/python/ba/_ads.py @@ -7,6 +7,7 @@ import time from typing import TYPE_CHECKING import _ba +from ba import _internal if TYPE_CHECKING: from typing import Callable, Any @@ -94,15 +95,15 @@ class AdsSubsystem: launch_count = app.config.get('launchCount', 0) # If we're seeing short ads we may want to space them differently. - interval_mult = (_ba.get_v1_account_misc_read_val( + 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 = _ba.get_v1_account_misc_read_val( + self.ad_amt = _internal.get_v1_account_misc_read_val( 'ads.startVal1', 0.99) else: - self.ad_amt = _ba.get_v1_account_misc_read_val( + self.ad_amt = _internal.get_v1_account_misc_read_val( 'ads.startVal2', 1.0) interval = None else: @@ -111,15 +112,17 @@ class AdsSubsystem: # (we reach our threshold faster the longer we've been # playing). base = 'ads' if _ba.has_video_ads() else 'ads2' - min_lc = _ba.get_v1_account_misc_read_val(base + '.minLC', 0.0) - max_lc = _ba.get_v1_account_misc_read_val(base + '.maxLC', 5.0) - min_lc_scale = (_ba.get_v1_account_misc_read_val( + min_lc = _internal.get_v1_account_misc_read_val( + base + '.minLC', 0.0) + max_lc = _internal.get_v1_account_misc_read_val( + base + '.maxLC', 5.0) + min_lc_scale = (_internal.get_v1_account_misc_read_val( base + '.minLCScale', 0.25)) - max_lc_scale = (_ba.get_v1_account_misc_read_val( + max_lc_scale = (_internal.get_v1_account_misc_read_val( base + '.maxLCScale', 0.34)) - min_lc_interval = (_ba.get_v1_account_misc_read_val( + min_lc_interval = (_internal.get_v1_account_misc_read_val( base + '.minLCInterval', 360)) - max_lc_interval = (_ba.get_v1_account_misc_read_val( + max_lc_interval = (_internal.get_v1_account_misc_read_val( base + '.maxLCInterval', 300)) if launch_count < min_lc: lc_amt = 0.0 diff --git a/dist/ba_data/python/ba/_app.py b/dist/ba_data/python/ba/_app.py index a0955c3..b976517 100644 --- a/dist/ba_data/python/ba/_app.py +++ b/dist/ba_data/python/ba/_app.py @@ -20,11 +20,13 @@ from ba._meta import MetadataSubsystem from ba._ads import AdsSubsystem from ba._net import NetworkSubsystem from ba._workspace import WorkspaceSubsystem +from ba import _internal if TYPE_CHECKING: import asyncio from typing import Any, Callable + import efro.log import ba from ba._cloud import CloudSubsystem from bastd.actor import spazappearance @@ -48,6 +50,7 @@ class App: # Implementations for these will be filled in by internal libs. accounts_v2: AccountV2Subsystem cloud: CloudSubsystem + log_handler: efro.log.LogHandler class State(Enum): """High level state the app can be in.""" @@ -91,6 +94,12 @@ class App: assert isinstance(self._env['build_number'], int) return self._env['build_number'] + @property + def device_name(self) -> str: + """Name of the device running the game.""" + assert isinstance(self._env['device_name'], str) + return self._env['device_name'] + @property def config_file_path(self) -> str: """Where the game's config file is stored on disk.""" @@ -223,6 +232,7 @@ class App: self._launch_completed = False self._initial_login_completed = False + self._meta_scan_completed = False self._called_on_app_running = False self._app_paused = False @@ -344,6 +354,7 @@ class App: from bastd.actor import spazappearance from ba._generated.enums import TimeType + assert _ba.in_logic_thread() self._aioloop = _asyncio.setup_asyncio() @@ -370,12 +381,12 @@ class App: # Non-test, non-debug builds should generally be blessed; warn if not. # (so I don't accidentally release a build that can't play tourneys) if (not self.debug_build and not self.test_build - and not _ba.is_blessed()): + and not _internal.is_blessed()): _ba.screenmessage('WARNING: NON-BLESSED BUILD', color=(1, 0, 0)) # If there's a leftover log file, attempt to upload it to the # master-server and/or get rid of it. - _apputils.handle_leftover_log_file() + _apputils.handle_leftover_v1_cloud_log_file() # Only do this stuff if our config file is healthy so we don't # overwrite a broken one or whatnot and wipe out data. @@ -408,7 +419,8 @@ class App: def check_special_offer() -> None: from bastd.ui.specialoffer import show_offer config = self.config - if ('pendingSpecialOffer' in config and _ba.get_public_login_id() + if ('pendingSpecialOffer' in config + and _internal.get_public_login_id() == config['pendingSpecialOffer']['a']): self.special_offer = config['pendingSpecialOffer']['o'] show_offer() @@ -416,6 +428,9 @@ class App: if not self.headless_mode: _ba.timer(3.0, check_special_offer, timetype=TimeType.REAL) + # Get meta-system scanning built-in stuff in the bg. + self.meta.start_scan(scan_complete_cb=self.on_meta_scan_complete) + self.accounts_v2.on_app_launch() self.accounts_v1.on_app_launch() @@ -430,17 +445,27 @@ class App: def on_app_running(self) -> None: """Called when initially entering the running state.""" - self.meta.on_app_running() self.plugins.on_app_running() # from ba._dependency import test_depset # test_depset() + def on_meta_scan_complete(self) -> None: + """Called by meta-scan when it is done doing its thing.""" + assert _ba.in_logic_thread() + self.plugins.on_meta_scan_complete() + + assert not self._meta_scan_completed + self._meta_scan_completed = True + self._update_state() + def _update_state(self) -> None: + assert _ba.in_logic_thread() + if self._app_paused: self.state = self.State.PAUSED else: - if self._initial_login_completed: + if self._initial_login_completed and self._meta_scan_completed: self.state = self.State.RUNNING if not self._called_on_app_running: self._called_on_app_running = True @@ -562,11 +587,11 @@ class App: # Kick off a little transaction so we'll hopefully have all the # latest account state when we get back to the menu. - _ba.add_transaction({ + _internal.add_transaction({ 'type': 'END_SESSION', 'sType': str(type(host_session)) }) - _ba.run_transactions() + _internal.run_transactions() host_session.end() @@ -651,5 +676,9 @@ class App: This should also run after a short amount of time if no login has occurred. """ + # Tell meta it can start scanning extra stuff that just showed up + # (account workspaces). + self.meta.start_extra_scan() + self._initial_login_completed = True self._update_state() diff --git a/dist/ba_data/python/ba/_appconfig.py b/dist/ba_data/python/ba/_appconfig.py index 26d46b7..4fd28de 100644 --- a/dist/ba_data/python/ba/_appconfig.py +++ b/dist/ba_data/python/ba/_appconfig.py @@ -128,12 +128,6 @@ def read_config() -> tuple[AppConfig, bool]: shutil.copyfile(config_file_path, config_file_path + '.broken') except Exception as exc2: print('EXC copying broken config:', exc2) - try: - _ba.log('broken config contents:\n' + - config_contents.replace('\000', ''), - to_stdout=False) - except Exception as exc2: - print('EXC logging broken config contents:', exc2) config = AppConfig() # Now attempt to read one of our 'prev' backup copies. @@ -159,8 +153,9 @@ 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.') return - _ba.mark_config_dirty() + mark_config_dirty() diff --git a/dist/ba_data/python/ba/_apputils.py b/dist/ba_data/python/ba/_apputils.py index de13b82..d7c2cfe 100644 --- a/dist/ba_data/python/ba/_apputils.py +++ b/dist/ba_data/python/ba/_apputils.py @@ -50,7 +50,7 @@ def should_submit_debug_info() -> bool: return _ba.app.config.get('Submit Debug Info', True) -def handle_log() -> None: +def handle_v1_cloud_log() -> None: """Called on debug log prints. When this happens, we can upload our log to the server @@ -58,6 +58,7 @@ def handle_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: @@ -73,7 +74,7 @@ def handle_log() -> None: activityname = 'unavailable' info = { - 'log': _ba.getlog(), + 'log': _ba.get_v1_cloud_log(), 'version': app.version, 'build': app.build_number, 'userAgentString': app.user_agent_string, @@ -82,8 +83,8 @@ def handle_log() -> None: 'fatal': 0, 'userRanCommands': _ba.has_user_run_commands(), 'time': _ba.time(TimeType.REAL), - 'userModded': _ba.has_user_mods(), - 'newsShow': _ba.get_news_show(), + 'userModded': _ba.workspaces_in_use(), + 'newsShow': get_news_show(), } def response(data: Any) -> None: @@ -107,7 +108,7 @@ def handle_log() -> None: def _reset() -> None: app.log_upload_timer_started = False if app.log_have_new: - handle_log() + handle_v1_cloud_log() if not _ba.is_log_full(): with _ba.Context('ui'): @@ -117,14 +118,15 @@ def handle_log() -> None: suppress_format_warning=True) -def handle_leftover_log_file() -> None: - """Handle an un-uploaded log from a previous run.""" +def handle_leftover_v1_cloud_log_file() -> None: + """Handle an un-uploaded v1-cloud-log from a previous run.""" try: import json from ba._net import master_server_post - if os.path.exists(_ba.get_log_file_path()): - with open(_ba.get_log_file_path(), encoding='utf-8') as infile: + 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: info = json.loads(infile.read()) infile.close() do_send = should_submit_debug_info() @@ -135,7 +137,7 @@ def handle_leftover_log_file() -> None: # lets kill it. if data is not None: try: - os.remove(_ba.get_log_file_path()) + os.remove(_ba.get_v1_cloud_log_file_path()) except FileNotFoundError: # Saw this in the wild. The file just existed # a moment ago but I suppose something could have @@ -145,7 +147,7 @@ def handle_leftover_log_file() -> None: master_server_post('bsLog', info, response) else: # If they don't want logs uploaded just kill it. - os.remove(_ba.get_log_file_path()) + os.remove(_ba.get_v1_cloud_log_file_path()) except Exception: from ba import _error _error.print_exception('Error handling leftover log file.') diff --git a/dist/ba_data/python/ba/_asyncio.py b/dist/ba_data/python/ba/_asyncio.py index c4ebc5e..3639d2a 100644 --- a/dist/ba_data/python/ba/_asyncio.py +++ b/dist/ba_data/python/ba/_asyncio.py @@ -18,7 +18,7 @@ import os if TYPE_CHECKING: import ba -# Our timer and event loop for the ballistica game thread. +# Our timer and event loop for the ballistica logic thread. _asyncio_timer: ba.Timer | None = None _asyncio_event_loop: asyncio.AbstractEventLoop | None = None @@ -33,7 +33,7 @@ def setup_asyncio() -> asyncio.AbstractEventLoop: import ba from ba._generated.enums import TimeType - assert _ba.in_game_thread() + assert _ba.in_logic_thread() # Create our event-loop. We don't expect there to be one # running on this thread before we do. diff --git a/dist/ba_data/python/ba/_bootstrap.py b/dist/ba_data/python/ba/_bootstrap.py new file mode 100644 index 0000000..88a4a4e --- /dev/null +++ b/dist/ba_data/python/ba/_bootstrap.py @@ -0,0 +1,175 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Bootstrapping.""" +from __future__ import annotations + +import os +import sys +from typing import TYPE_CHECKING + +from efro.log import setup_logging, LogLevel +import _ba + +if TYPE_CHECKING: + from typing import Any + from efro.log import LogEntry + +_g_did_bootstrap = False # pylint: disable=invalid-name + + +def bootstrap() -> None: + """Run bootstrapping logic. + + This is the very first ballistica code that runs (aside from imports). + It sets up low level environment bits and creates the app instance. + """ + + global _g_did_bootstrap # pylint: disable=global-statement, invalid-name + if _g_did_bootstrap: + raise RuntimeError('Bootstrap has already been called.') + _g_did_bootstrap = True + + # The first thing we do is set up our logging system and feed + # Python's stdout/stderr into it. Then we can at least debug problems + # on systems where native stdout/stderr is not easily accessible + # such as Android. + log_handler = setup_logging(log_path=None, + level=LogLevel.DEBUG, + suppress_non_root_debug=True, + log_stdout_stderr=True, + cache_size_limit=1024 * 1024) + + log_handler.add_callback(_on_log) + + env = _ba.env() + + # Give a soft warning if we're being used with a different binary + # version than we expect. + expected_build = 20882 + running_build: int = env['build_number'] + if running_build != expected_build: + print( + f'WARNING: These script files are meant to be used with' + f' Ballistica build {expected_build}.\n' + f' You are running build {running_build}.' + f' This might cause the app to error or misbehave.', + file=sys.stderr) + + # In bootstrap_monolithic.py we told Python not to handle SIGINT itself + # (because that must be done in the main thread). Now we finish the + # job by adding our own handler to replace it. + + # Note: I've found we need to set up our C signal handling AFTER + # we've told Python to disable its own; otherwise (on Mac at least) it + # wipes out our existing C handler. + _ba.setup_sigint() + + # Sanity check: we should always be run in UTF-8 mode. + if sys.flags.utf8_mode != 1: + print( + 'ERROR: Python\'s UTF-8 mode is not set.' + ' This will likely result in errors.', + file=sys.stderr) + + debug_build = env['debug_build'] + + # We expect dev_mode on in debug builds and off otherwise. + if debug_build != sys.flags.dev_mode: + print( + f'WARNING: Mismatch in debug_build {debug_build}' + f' and sys.flags.dev_mode {sys.flags.dev_mode}', + file=sys.stderr) + + # In embedded situations (when we're providing our own Python) let's + # also provide our own root certs so ssl works. We can consider overriding + # this in particular embedded cases if we can verify that system certs + # are working. + # (We also allow forcing this via an env var if the user desires) + if (_ba.contains_python_dist() + or os.environ.get('BA_USE_BUNDLED_ROOT_CERTS') == '1'): + import certifi + + # Let both OpenSSL and requests (if present) know to use this. + os.environ['SSL_CERT_FILE'] = os.environ['REQUESTS_CA_BUNDLE'] = ( + certifi.where()) + + # On Windows I'm seeing the following error creating asyncio loops in + # background threads with the default proactor setup: + # ValueError: set_wakeup_fd only works in main thread of the main + # interpreter + # So let's explicitly request selector loops. + # Interestingly this error only started showing up once I moved + # Python init to the main thread; previously the various asyncio + # bg thread loops were working fine (maybe something caused them + # to default to selector in that case?.. + if sys.platform == 'win32': + import asyncio + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + + # pylint: disable=c-extension-no-member + if not TYPE_CHECKING: + import __main__ + + # Clear out the standard quit/exit messages since they don't + # work for us. + del __main__.__builtins__.quit + del __main__.__builtins__.exit + + # Also replace standard interactive help with our simplified + # one which is more friendly to cloud/in-game console situations. + __main__.__builtins__.help = _CustomHelper() + + # Now spin up our App instance and store it on both _ba and ba. + from ba._app import App + import ba + _ba.app = ba.app = App() + _ba.app.log_handler = log_handler + + +class _CustomHelper: + """Replacement 'help' that behaves better for our setup.""" + + def __repr__(self) -> str: + return 'Type help(object) for help about object.' + + def __call__(self, *args: Any, **kwds: Any) -> Any: + # We get an ugly error importing pydoc on our embedded + # platforms due to _sysconfigdata_xxx.py not being present + # (but then things mostly work). Let's get the ugly error out + # of the way explicitly. + import sysconfig + try: + # This errors once but seems to run cleanly after, so let's + # get the error out of the way. + sysconfig.get_path('stdlib') + except ModuleNotFoundError: + pass + + import pydoc + # Disable pager and interactive help since neither works well + # with our funky multi-threaded setup or in-game/cloud consoles. + # Let's just do simple text dumps. + pydoc.pager = pydoc.plainpager + if not args and not kwds: + print('Interactive help is not available in this environment.\n' + 'Type help(object) for help about object.') + return None + return pydoc.help(*args, **kwds) + + +def _on_log(entry: LogEntry) -> None: + + # Just forward this along to the engine to display in the in-game console, + # in the Android log, etc. + _ba.display_log( + name=entry.name, + level=entry.level.name, + message=entry.message, + ) + + # We also want to feed some logs to the old V1-cloud-log system. + # Let's go with anything warning or higher as well as the stdout/stderr + # log messages that ba.app.log_handler creates for us. + if entry.level.value >= LogLevel.WARNING.value or entry.name in ('stdout', + 'stderr'): + _ba.v1_cloud_log(entry.message) diff --git a/dist/ba_data/python/ba/_cloud.py b/dist/ba_data/python/ba/_cloud.py index e2149a3..ebbd99a 100644 --- a/dist/ba_data/python/ba/_cloud.py +++ b/dist/ba_data/python/ba/_cloud.py @@ -99,3 +99,46 @@ class CloudSubsystem: Must be called from a background thread. """ raise RuntimeError('Cloud functionality is not available.') + + +def cloud_console_exec(code: str) -> None: + """Called by the cloud console to run code in the logic thread.""" + import sys + import logging + import __main__ + from ba._generated.enums import TimeType + try: + + # First try it as eval. + try: + evalcode = compile(code, '', 'eval') + except SyntaxError: + evalcode = None + except Exception: + # 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.') + evalcode = None + if evalcode is not None: + # pylint: disable=eval-used + value = eval(evalcode, vars(__main__), vars(__main__)) + # For eval-able statements, print the resulting value if + # it is not None (just like standard Python interpreter). + if value is not None: + print(repr(value), file=sys.stderr) + + # Fall back to exec if we couldn't compile it as eval. + else: + execcode = compile(code, '', 'exec') + # pylint: disable=exec-used + 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() + + # This helps the logging system ship stderr back to the + # cloud promptly. + sys.stderr.flush() diff --git a/dist/ba_data/python/ba/_coopgame.py b/dist/ba_data/python/ba/_coopgame.py index f48fbc5..6e380d8 100644 --- a/dist/ba_data/python/ba/_coopgame.py +++ b/dist/ba_data/python/ba/_coopgame.py @@ -6,6 +6,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, TypeVar import _ba +from ba import _internal from ba._gameactivity import GameActivity from ba._general import WeakCall @@ -54,19 +55,6 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]): # Preload achievement images in case we get some. _ba.timer(2.0, WeakCall(self._preload_achievements)) - # Let's ask the server for a 'time-to-beat' value. - levelname = self._get_coop_level_name() - campaign = self.session.campaign - assert campaign is not None - config_str = (str(len(self.players)) + 'p' + campaign.getlevel( - self.settings_raw['name']).get_score_version_string().replace( - ' ', '_')) - _ba.get_scores_to_beat(levelname, config_str, - WeakCall(self._on_got_scores_to_beat)) - - def _on_got_scores_to_beat(self, scores: list[dict[str, Any]]) -> None: - pass - def _show_standard_scores_to_beat_ui(self, scores: list[dict[str, Any]]) -> None: from efro.util import asserttype @@ -217,10 +205,10 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]): self._achievements_awarded.add(achievement_name) # Report new achievements to the game-service. - _ba.report_achievement(achievement_name) + _internal.report_achievement(achievement_name) # ...and to our account. - _ba.add_transaction({ + _internal.add_transaction({ 'type': 'ACHIEVEMENT', 'name': achievement_name }) diff --git a/dist/ba_data/python/ba/_error.py b/dist/ba_data/python/ba/_error.py index bb82a78..5e6aba4 100644 --- a/dist/ba_data/python/ba/_error.py +++ b/dist/ba_data/python/ba/_error.py @@ -125,6 +125,11 @@ class WidgetNotFoundError(NotFoundError): """ +# TODO: Should integrate some sort of context printing into our +# log handling so we can just use logging.exception() and kill these +# two functions. + + def print_exception(*args: Any, **keywds: Any) -> None: """Print info about an exception along with pertinent context state. diff --git a/dist/ba_data/python/ba/_gameactivity.py b/dist/ba_data/python/ba/_gameactivity.py index 0d22185..46dcdf0 100644 --- a/dist/ba_data/python/ba/_gameactivity.py +++ b/dist/ba_data/python/ba/_gameactivity.py @@ -9,6 +9,7 @@ import random from typing import TYPE_CHECKING, TypeVar import _ba +from ba import _internal from ba._activity import Activity from ba._score import ScoreConfig from ba._language import Lstr @@ -17,6 +18,7 @@ from ba._error import NotFoundError, print_error, print_exception from ba._general import Call, WeakCall from ba._player import PlayerInfo from ba import _map +from ba import _store if TYPE_CHECKING: from typing import Any, Callable, Sequence @@ -239,11 +241,11 @@ class GameActivity(Activity[PlayerType, TeamType]): self._zoom_message_times: dict[int, float] = {} self._is_waiting_for_continue = False - self._continue_cost = _ba.get_v1_account_misc_read_val( + self._continue_cost = _internal.get_v1_account_misc_read_val( 'continueStartCost', 25) - self._continue_cost_mult = _ba.get_v1_account_misc_read_val( + self._continue_cost_mult = _internal.get_v1_account_misc_read_val( 'continuesMult', 2) - self._continue_cost_offset = _ba.get_v1_account_misc_read_val( + self._continue_cost_offset = _internal.get_v1_account_misc_read_val( 'continuesOffset', 0) @property @@ -363,11 +365,11 @@ class GameActivity(Activity[PlayerType, TeamType]): if do_continue: _ba.playsound(_ba.getsound('shieldUp')) _ba.playsound(_ba.getsound('cashRegister')) - _ba.add_transaction({ + _internal.add_transaction({ 'type': 'CONTINUE', 'cost': self._continue_cost }) - _ba.run_transactions() + _internal.run_transactions() self._continue_cost = ( self._continue_cost * self._continue_cost_mult + self._continue_cost_offset) @@ -390,7 +392,8 @@ class GameActivity(Activity[PlayerType, TeamType]): from ba._generated.enums import TimeType try: - if _ba.get_v1_account_misc_read_val('enableContinues', False): + if _internal.get_v1_account_misc_read_val('enableContinues', + False): session = self.session # We only support continuing in non-tournament games. @@ -453,7 +456,7 @@ class GameActivity(Activity[PlayerType, TeamType]): # time is left. tournament_id = self.session.tournament_id if tournament_id is not None: - _ba.tournament_query( + _internal.tournament_query( args={ 'tournamentIDs': [tournament_id], 'source': 'in-game time remaining query' @@ -1159,7 +1162,7 @@ class GameActivity(Activity[PlayerType, TeamType]): else: # If settings doesn't specify a map, pick a random one from the # list of supported ones. - unowned_maps = _map.get_unowned_maps() + unowned_maps = _store.get_unowned_maps() valid_maps: list[str] = [ m for m in self.get_supported_maps(type(self.session)) if m not in unowned_maps diff --git a/dist/ba_data/python/ba/_general.py b/dist/ba_data/python/ba/_general.py index d32b933..f40fa09 100644 --- a/dist/ba_data/python/ba/_general.py +++ b/dist/ba_data/python/ba/_general.py @@ -31,13 +31,11 @@ class Existable(Protocol): """Whether this object exists.""" -# pylint: disable=invalid-name -ExistableType = TypeVar('ExistableType', bound=Existable) -# pylint: enable=invalid-name +ExistableT = TypeVar('ExistableT', bound=Existable) T = TypeVar('T') -def existing(obj: ExistableType | None) -> ExistableType | None: +def existing(obj: ExistableT | None) -> ExistableT | None: """Convert invalid references to None for any ba.Existable object. Category: **Gameplay Functions** @@ -251,6 +249,10 @@ class _Call: if TYPE_CHECKING: + # Some interaction between our ballistica pylint plugin + # and this code is crashing starting on pylint 2.15.0. + # This seems to fix things for now. + # pylint: disable=all WeakCall = Call Call = Call else: diff --git a/dist/ba_data/python/ba/_hooks.py b/dist/ba_data/python/ba/_hooks.py index ee45ca7..e02ba22 100644 --- a/dist/ba_data/python/ba/_hooks.py +++ b/dist/ba_data/python/ba/_hooks.py @@ -16,6 +16,7 @@ from __future__ import annotations from typing import TYPE_CHECKING import _ba +from ba import _internal if TYPE_CHECKING: from typing import Sequence, Any @@ -24,10 +25,10 @@ if TYPE_CHECKING: def finish_bootstrapping() -> None: """Do final bootstrapping related bits.""" - assert _ba.in_game_thread() + assert _ba.in_logic_thread() # Kick off our asyncio event handling, allowing us to use coroutines - # in our game thread alongside our internal event handling. + # in our logic thread alongside our internal event handling. # setup_asyncio() # Ok, bootstrapping is done; time to get the show started. @@ -189,8 +190,8 @@ def unavailable_message() -> None: def submit_analytics_counts(sval: str) -> None: - _ba.add_transaction({'type': 'ANALYTICS_COUNTS', 'values': sval}) - _ba.run_transactions() + _internal.add_transaction({'type': 'ANALYTICS_COUNTS', 'values': sval}) + _internal.run_transactions() def set_last_ad_network(sval: str) -> None: diff --git a/dist/ba_data/python/ba/_input.py b/dist/ba_data/python/ba/_input.py index bfea728..fc21a43 100644 --- a/dist/ba_data/python/ba/_input.py +++ b/dist/ba_data/python/ba/_input.py @@ -6,6 +6,7 @@ from __future__ import annotations from typing import TYPE_CHECKING import _ba +from ba._internal import get_v1_account_display_string if TYPE_CHECKING: from typing import Any @@ -639,5 +640,5 @@ def get_last_player_name_from_input_device(device: ba.InputDevice) -> str: if profilename == '_random': profilename = device.get_default_player_name() if profilename == '__account__': - profilename = _ba.get_v1_account_display_string() + profilename = get_v1_account_display_string() return profilename diff --git a/dist/ba_data/python/ba/_internal.py b/dist/ba_data/python/ba/_internal.py new file mode 100644 index 0000000..5637e6a --- /dev/null +++ b/dist/ba_data/python/ba/_internal.py @@ -0,0 +1,367 @@ +# Released under the MIT License. See LICENSE for details. +# +"""A soft wrapper around _bainternal. + +This allows code to use _bainternal functionality and get warnings +or fallbacks in some cases instead of hard errors. Code that absolutely +relies on the presence of _bainternal can just use that module directly. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +try: + # noinspection PyUnresolvedReferences + import _bainternal + HAVE_INTERNAL = True +except ImportError: + HAVE_INTERNAL = False + +if TYPE_CHECKING: + from typing import Callable, Any + + +# Code that will function without _bainternal but which should be updated +# to account for its absence should call this to draw attention to itself. +def _no_bainternal_warning() -> None: + import logging + logging.warning('INTERNAL CALL RUN WITHOUT INTERNAL PRESENT.') + + +# Code that won't work without _bainternal should raise these errors. +def _no_bainternal_error() -> RuntimeError: + raise RuntimeError('_bainternal is not present') + + +def get_v2_fleet() -> str: + """(internal)""" + if HAVE_INTERNAL: + return _bainternal.get_v2_fleet() + raise _no_bainternal_error() + + +def get_master_server_address(source: int = -1, version: int = 1) -> str: + """(internal) + + Return the address of the master server. + """ + if HAVE_INTERNAL: + return _bainternal.get_master_server_address(source=source, + version=version) + raise _no_bainternal_error() + + +def is_blessed() -> bool: + """(internal)""" + if HAVE_INTERNAL: + return _bainternal.is_blessed() + + # Harmless to always just say no here. + return False + + +def get_news_show() -> str: + """(internal)""" + if HAVE_INTERNAL: + return _bainternal.get_news_show() + raise _no_bainternal_error() + + +def game_service_has_leaderboard(game: str, config: str) -> bool: + """(internal) + + Given a game and config string, returns whether there is a leaderboard + for it on the game service. + """ + if HAVE_INTERNAL: + return _bainternal.game_service_has_leaderboard(game=game, + config=config) + # Harmless to always just say no here. + return False + + +def report_achievement(achievement: str, pass_to_account: bool = True) -> None: + """(internal)""" + if HAVE_INTERNAL: + _bainternal.report_achievement(achievement=achievement, + pass_to_account=pass_to_account) + return + + # Need to see if this actually still works as expected.. warning for now. + _no_bainternal_warning() + + +# noinspection PyUnresolvedReferences +def submit_score(game: str, + config: str, + name: Any, + score: int | None, + callback: Callable, + 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. + As a courtesy, please don't send fake scores to the server. I'd prefer + to devote my time to improving the game instead of trying to make the + score server more mischief-proof. + """ + if HAVE_INTERNAL: + _bainternal.submit_score(game=game, + config=config, + name=name, + score=score, + callback=callback, + 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: + """(internal)""" + if HAVE_INTERNAL: + _bainternal.tournament_query(callback=callback, args=args) + return + + # This technically breaks since callback will never be called/etc. + raise _no_bainternal_error() + + +def power_ranking_query(callback: Callable, season: Any = None) -> None: + """(internal)""" + if HAVE_INTERNAL: + _bainternal.power_ranking_query(callback=callback, season=season) + return + + # This technically breaks since callback will never be called/etc. + raise _no_bainternal_error() + + +def restore_purchases() -> None: + """(internal)""" + if HAVE_INTERNAL: + _bainternal.restore_purchases() + return + + # This shouldn't break anything but should try to avoid calling it. + _no_bainternal_warning() + + +def purchase(item: str) -> None: + """(internal)""" + + if HAVE_INTERNAL: + _bainternal.purchase(item) + return + + # This won't break messily but won't function as intended. + _no_bainternal_warning() + + +def get_purchases_state() -> int: + """(internal)""" + + if HAVE_INTERNAL: + return _bainternal.get_purchases_state() + + # This won't function correctly without internal. + raise _no_bainternal_error() + + +def get_purchased(item: str) -> bool: + """(internal)""" + + if HAVE_INTERNAL: + return _bainternal.get_purchased(item) + + # Without internal we can just assume no purchases. + return False + + +def get_price(item: str) -> str | None: + """(internal)""" + + if HAVE_INTERNAL: + return _bainternal.get_price(item) + + # Without internal we can just assume no prices. + return None + + +def in_game_purchase(item: str, price: int) -> None: + """(internal)""" + + if HAVE_INTERNAL: + _bainternal.in_game_purchase(item=item, price=price) + return + + # Without internal this doesn't function as expected. + raise _no_bainternal_error() + + +# noinspection PyUnresolvedReferences +def add_transaction(transaction: dict, + callback: Callable | None = None) -> None: + """(internal)""" + if HAVE_INTERNAL: + _bainternal.add_transaction(transaction=transaction, callback=callback) + return + + # This won't function correctly without internal (callback never called). + raise _no_bainternal_error() + + +def reset_achievements() -> None: + """(internal)""" + if HAVE_INTERNAL: + _bainternal.reset_achievements() + return + + # Technically doesnt break but won't do anything. + _no_bainternal_warning() + + +def get_public_login_id() -> str | None: + """(internal)""" + + if HAVE_INTERNAL: + return _bainternal.get_public_login_id() + + # Harmless to return nothing in this case. + return None + + +def have_outstanding_transactions() -> bool: + """(internal)""" + + if HAVE_INTERNAL: + return _bainternal.have_outstanding_transactions() + + # Harmless to return False here. + return False + + +def run_transactions() -> None: + """(internal)""" + if HAVE_INTERNAL: + _bainternal.run_transactions() + + # Harmless no-op in this case. + + +def get_v1_account_misc_read_val(name: str, default_value: Any) -> Any: + """(internal)""" + if HAVE_INTERNAL: + return _bainternal.get_v1_account_misc_read_val( + name=name, default_value=default_value) + raise _no_bainternal_error() + + +def get_v1_account_misc_read_val_2(name: str, default_value: Any) -> Any: + """(internal)""" + if HAVE_INTERNAL: + return _bainternal.get_v1_account_misc_read_val_2( + name=name, default_value=default_value) + raise _no_bainternal_error() + + +def get_v1_account_misc_val(name: str, default_value: Any) -> Any: + """(internal)""" + if HAVE_INTERNAL: + return _bainternal.get_v1_account_misc_val(name=name, + default_value=default_value) + raise _no_bainternal_error() + + +def get_v1_account_ticket_count() -> int: + """(internal) + + Returns the number of tickets for the current account. + """ + + if HAVE_INTERNAL: + return _bainternal.get_v1_account_ticket_count() + return 0 + + +def get_v1_account_state_num() -> int: + """(internal)""" + if HAVE_INTERNAL: + return _bainternal.get_v1_account_state_num() + return 0 + + +def get_v1_account_state() -> str: + """(internal)""" + if HAVE_INTERNAL: + return _bainternal.get_v1_account_state() + raise _no_bainternal_error() + + +def get_v1_account_display_string(full: bool = True) -> str: + """(internal)""" + if HAVE_INTERNAL: + return _bainternal.get_v1_account_display_string(full=full) + raise _no_bainternal_error() + + +def get_v1_account_type() -> str: + """(internal)""" + if HAVE_INTERNAL: + return _bainternal.get_v1_account_type() + raise _no_bainternal_error() + + +def get_v1_account_name() -> str: + """(internal)""" + if HAVE_INTERNAL: + return _bainternal.get_v1_account_name() + raise _no_bainternal_error() + + +def sign_out_v1(v2_embedded: bool = False) -> None: + """(internal) + + Category: General Utility Functions + """ + if HAVE_INTERNAL: + _bainternal.sign_out_v1(v2_embedded=v2_embedded) + return + raise _no_bainternal_error() + + +def sign_in_v1(account_type: str) -> None: + """(internal) + + Category: General Utility Functions + """ + if HAVE_INTERNAL: + _bainternal.sign_in_v1(account_type=account_type) + return + raise _no_bainternal_error() + + +def mark_config_dirty() -> None: + """(internal) + + Category: General Utility Functions + """ + if HAVE_INTERNAL: + _bainternal.mark_config_dirty() + return + + # Note to self - need to fix config writing to not rely on + # internal lib. + _no_bainternal_warning() diff --git a/dist/ba_data/python/ba/_map.py b/dist/ba_data/python/ba/_map.py index b8510a0..2608250 100644 --- a/dist/ba_data/python/ba/_map.py +++ b/dist/ba_data/python/ba/_map.py @@ -101,22 +101,6 @@ def getmaps(playtype: str) -> list[str]: if playtype in val.get_play_types()) -def get_unowned_maps() -> list[str]: - """Return the list of local maps not owned by the current account. - - Category: **Asset Functions** - """ - from ba import _store - unowned_maps: set[str] = set() - if not _ba.app.headless_mode: - for map_section in _store.get_store_layout()['maps']: - for mapitem in map_section['items']: - if not _ba.get_purchased(mapitem): - m_info = _store.get_store_item(mapitem) - unowned_maps.add(m_info['map_type'].name) - return sorted(unowned_maps) - - def get_map_class(name: str) -> type[ba.Map]: """Return a map type given a name. diff --git a/dist/ba_data/python/ba/_meta.py b/dist/ba_data/python/ba/_meta.py index 408be18..7b43f9b 100644 --- a/dist/ba_data/python/ba/_meta.py +++ b/dist/ba_data/python/ba/_meta.py @@ -6,33 +6,50 @@ from __future__ import annotations import os import time -import threading +import logging +from threading import Thread from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, TypeVar from dataclasses import dataclass, field +from efro.call import tpartial import _ba if TYPE_CHECKING: - import ba + from typing import Callable # The meta api version of this build of the game. # Only packages and modules requiring this exact api version # will be considered when scanning directories. + # See: https://ballistica.net/wiki/Meta-Tags CURRENT_API_VERSION = 6 #TODO update it to latest # current API version is 7 , im downgrading it to 6 to support mini games which i cant update to 7 bcoz of encryption # shouldn't be a issue , I manually updated all plugin on_app_launch to on_app_running and that was the only change btw API 6 and 7 +# Meta export lines can use these names to represent these classes. +# This is purely a convenience; it is possible to use full class paths +# instead of these or to make the meta system aware of arbitrary classes. +EXPORT_CLASS_NAME_SHORTCUTS: dict[str, str] = { + 'plugin': 'ba.Plugin', + 'keyboard': 'ba.Keyboard', + 'game': 'ba.GameActivity', +} + +T = TypeVar('T') + + @dataclass class ScanResults: - """Final results from a metadata scan.""" - games: list[str] = field(default_factory=list) - plugins: list[str] = field(default_factory=list) - keyboards: list[str] = field(default_factory=list) - errors: str = '' - warnings: str = '' + """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) + + def exports_of_class(self, cls: type) -> list[str]: + """Return exports of a given class.""" + return self.exports.get(f'{cls.__module__}.{cls.__qualname__}', []) class MetadataSubsystem: @@ -44,99 +61,98 @@ class MetadataSubsystem: """ def __init__(self) -> None: - self.scanresults: ScanResults | None = None + + self._scan: DirectoryScan | None = None + + # Can be populated before starting the scan. self.extra_scan_dirs: list[str] = [] - def on_app_running(self) -> None: - """Should be called when the app enters the running state.""" + # Results populated once scan is complete. + self.scanresults: ScanResults | None = None - # Start scanning for things exposed via ba_meta. - self.start_scan() + self._scan_complete_cb: Callable[[], None] | None = None - def start_scan(self) -> None: - """Begin scanning script directories for scripts containing metadata. + def start_scan(self, scan_complete_cb: Callable[[], None]) -> None: + """Begin the overall scan. - Should be called only once at launch.""" - app = _ba.app - if self.scanresults is not None: - print('WARNING: meta scan run more than once.') - pythondirs = ([app.python_directory_app, app.python_directory_user] + - self.extra_scan_dirs) - thread = ScanThread(pythondirs) - thread.start() + This will start scanning built in directories (which for vanilla + installs should be the vast majority of the work). This should only + be called once. + """ + assert self._scan_complete_cb is None + assert self._scan is None - def handle_scan_results(self, results: ScanResults) -> None: - """Called in the game thread with results of a completed scan.""" + self._scan_complete_cb = scan_complete_cb + self._scan = DirectoryScan( + [_ba.app.python_directory_app, _ba.app.python_directory_user]) - from ba._language import Lstr - from ba._plugin import PotentialPlugin + Thread(target=self._run_scan_in_bg, daemon=True).start() - # Warnings generally only get printed locally for users' benefit - # (things like out-of-date scripts being ignored, etc.) - # Errors are more serious and will get included in the regular log - # warnings = results.get('warnings', '') - # errors = results.get('errors', '') - if results.warnings != '' or results.errors != '': - import textwrap - _ba.screenmessage(Lstr(resource='scanScriptsErrorText'), - color=(1, 0, 0)) - _ba.playsound(_ba.getsound('error')) - if results.warnings != '': - _ba.log(textwrap.indent(results.warnings, - 'Warning (meta-scan): '), - to_server=False) - if results.errors != '': - _ba.log(textwrap.indent(results.errors, 'Error (meta-scan): ')) + def start_extra_scan(self) -> None: + """Proceed to the extra_scan_dirs portion of the scan. - # Handle plugins. - plugs = _ba.app.plugins - config_changed = False - found_new = False - plugstates: dict[str, dict] = _ba.app.config.setdefault('Plugins', {}) - assert isinstance(plugstates, dict) + This is for parts of the scan that must be delayed until + workspace sync completion or other such events. This must be + called exactly once. + """ + assert self._scan is not None + self._scan.set_extras(self.extra_scan_dirs) - # Create a potential-plugin for each class we found in the scan. - for class_path in results.plugins: - plugs.potential_plugins.append( - PotentialPlugin(display_name=Lstr(value=class_path), - class_path=class_path, - available=True)) - if class_path not in plugstates: - # Go ahead and enable new plugins by default, but we'll - # inform the user that they need to restart to pick them up. - # they can also disable them in settings so they never load. - plugstates[class_path] = {'enabled': True} - config_changed = True - found_new = True + def load_exported_classes( + self, + cls: type[T], + completion_cb: Callable[[list[type[T]]], None], + completion_cb_in_bg_thread: bool = False, + ) -> None: + """High level function to load meta-exported classes. - # Also add a special one for any plugins set to load but *not* found - # in the scan (this way they will show up in the UI so we can disable - # them) - for class_path, plugstate in plugstates.items(): - enabled = plugstate.get('enabled', False) - assert isinstance(enabled, bool) - if enabled and class_path not in results.plugins: - plugs.potential_plugins.append( - PotentialPlugin(display_name=Lstr(value=class_path), - class_path=class_path, - available=False)) + Will wait for scanning to complete if necessary, and will load all + registered classes of a particular type in a background thread before + calling the passed callback in the logic thread. Errors may be logged + to messaged to the user in some way but the callback will be called + regardless. + To run the completion callback directly in the bg thread where the + 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), + daemon=True, + ).start() - plugs.potential_plugins.sort(key=lambda p: p.class_path) + def _load_exported_classes( + self, + cls: type[T], + completion_cb: Callable[[list[type[T]]], None], + 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) + for classname in classnames: + try: + classes.append(getclass(classname, cls)) + except Exception: + logging.exception('error importing %s', classname) - if found_new: - _ba.screenmessage(Lstr(resource='pluginsDetectedText'), - color=(0, 1, 0)) - _ba.playsound(_ba.getsound('ding')) + except Exception: + logging.exception('Error loading exported classes.') - if config_changed: - _ba.app.config.commit() + completion_call = tpartial(completion_cb, classes) + if completion_cb_in_bg_thread: + completion_call() + else: + _ba.pushcall(completion_call, from_other_thread=True) - def get_scan_results(self) -> ScanResults: - """Return meta scan results; block if the scan is not yet complete.""" + def _wait_for_scan_results(self) -> ScanResults: + """Return scan results, blocking if the scan is not yet complete.""" if self.scanresults is None: - print('WARNING: ba.meta.get_scan_results()' - ' called before scan completed.' - ' This can cause hitches.') + if _ba.in_logic_thread(): + logging.warning( + 'ba.meta._wait_for_scan_results()' + ' called in logic thread before scan completed;' + ' this can cause hitches.') # Now wait a bit for the scan to complete. # Eventually error though if it doesn't. @@ -148,69 +164,53 @@ class MetadataSubsystem: 'timeout waiting for meta scan to complete.') return self.scanresults - def get_game_types(self) -> list[type[ba.GameActivity]]: - """Return available game types.""" - from ba._general import getclass - from ba._gameactivity import GameActivity - gameclassnames = self.get_scan_results().games - gameclasses = [] - for gameclassname in gameclassnames: - try: - cls = getclass(gameclassname, GameActivity) - gameclasses.append(cls) - except Exception: - from ba import _error - _error.print_exception('error importing ' + str(gameclassname)) - unowned = self.get_unowned_game_types() - return [cls for cls in gameclasses if cls not in unowned] - - def get_unowned_game_types(self) -> set[type[ba.GameActivity]]: - """Return present game types not owned by the current account.""" + def _run_scan_in_bg(self) -> None: + """Runs a scan (for use in background thread).""" try: - from ba import _store - unowned_games: set[type[ba.GameActivity]] = set() - if not _ba.app.headless_mode: - for section in _store.get_store_layout()['minigames']: - for mname in section['items']: - if not _ba.get_purchased(mname): - m_info = _store.get_store_item(mname) - unowned_games.add(m_info['gametype']) - return unowned_games - except Exception: - from ba import _error - _error.print_exception('error calcing un-owned games') - return set() - - -class ScanThread(threading.Thread): - """Thread to scan script dirs for metadata.""" - - def __init__(self, dirs: list[str]): - super().__init__() - self._dirs = dirs - - def run(self) -> None: - from ba._general import Call - try: - scan = DirectoryScan(self._dirs) - scan.scan() - results = scan.results + assert self._scan is not None + self._scan.run() + results = self._scan.results + self._scan = None except Exception as exc: - results = ScanResults(errors=f'Scan exception: {exc}') + results = ScanResults(errors=[f'Scan exception: {exc}']) - # Push a call to the game thread to print warnings/errors - # or otherwise deal with scan results. - _ba.pushcall(Call(_ba.app.meta.handle_scan_results, results), - from_other_thread=True) + # Place results and tell the logic thread they're ready. + self.scanresults = results + _ba.pushcall(self._handle_scan_results, from_other_thread=True) - # We also, however, immediately make results available. - # This is because the game thread may be blocked waiting - # for them so we can't push a call or we'd get deadlock. - _ba.app.meta.scanresults = results + 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 + assert results is not None + + # Spit out any warnings/errors that happened. + # Warnings generally only get printed locally for users' benefit + # (things like out-of-date scripts being ignored, etc.) + # Errors are more serious and will get included in the regular log. + if results.warnings or results.errors: + import textwrap + _ba.screenmessage(Lstr(resource='scanScriptsErrorText'), + color=(1, 0, 0)) + _ba.playsound(_ba.getsound('error')) + if results.warnings: + 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): ') + logging.error(allerrors) + + # Let the game know we're done. + assert self._scan_complete_cb is not None + self._scan_complete_cb() class DirectoryScan: - """Handles scanning directories for metadata.""" + """Scans directories for metadata.""" def __init__(self, paths: list[str]): """Given one or more paths, parses available meta information. @@ -220,9 +220,42 @@ class DirectoryScan: """ # Skip non-existent paths completely. - self.paths = [Path(p) for p in paths if os.path.isdir(p)] + self.base_paths = [Path(p) for p in paths if os.path.isdir(p)] + self.extra_paths: list[Path] = [] + self.extra_paths_set = False self.results = ScanResults() + def set_extras(self, paths: list[str]) -> None: + """Set extra portion.""" + # Skip non-existent paths completely. + self.extra_paths += [Path(p) for p in paths if os.path.isdir(p)] + self.extra_paths_set = True + + def run(self) -> None: + """Do the thing.""" + for pathlist in [self.base_paths, self.extra_paths]: + + # Spin and wait until extra paths are provided before doing them. + if pathlist is self.extra_paths: + while not self.extra_paths_set: + time.sleep(0.001) + + modules: list[tuple[Path, Path]] = [] + for path in pathlist: + self._get_path_module_entries(path, '', modules) + for moduledir, subpath in modules: + try: + self._scan_module(moduledir, subpath) + except Exception: + import traceback + self.results.warnings.append( + 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: """Scan provided path and add module entries to provided list.""" @@ -237,7 +270,7 @@ class DirectoryScan: entries = [] except Exception as exc: # Unexpected; report this. - self.results.errors += f'{exc}\n' + self.results.errors.append(str(exc)) entries = [] # Now identify python packages/modules out of what we found. @@ -248,24 +281,7 @@ class DirectoryScan: and Path(entry[0], entry[1], '__init__.py').is_file()): modules.append(entry) - def scan(self) -> None: - """Scan provided paths.""" - modules: list[tuple[Path, Path]] = [] - for path in self.paths: - self._get_path_module_entries(path, '', modules) - for moduledir, subpath in modules: - try: - self.scan_module(moduledir, subpath) - except Exception: - import traceback - self.results.warnings += ("Error scanning '" + str(subpath) + - "': " + traceback.format_exc() + - '\n') - # Sort our results - self.results.games.sort() - self.results.plugins.sort() - - def scan_module(self, moduledir: Path, subpath: Path) -> None: + def _scan_module(self, moduledir: Path, subpath: Path) -> None: """Scan an individual module and add the findings to results.""" if subpath.name.endswith('.py'): fpath = Path(moduledir, subpath) @@ -279,11 +295,12 @@ class DirectoryScan: lnum: l[1:].split() for lnum, l in enumerate(flines) if '# ba_meta ' in l } - toplevel = len(subpath.parts) <= 1 - required_api = self.get_api_requirement(subpath, meta_lines, toplevel) + is_top_level = len(subpath.parts) <= 1 + required_api = self._get_api_requirement(subpath, meta_lines, + is_top_level) # Top level modules with no discernible api version get ignored. - if toplevel and required_api is None: + if is_top_level and required_api is None: return # If we find a module requiring a different api version, warn @@ -291,7 +308,7 @@ 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.\n') + f' we are running {CURRENT_API_VERSION}; ignoring module.') return # Ok; can proceed with a full scan of this module. @@ -304,11 +321,11 @@ class DirectoryScan: self._get_path_module_entries(moduledir, subpath, submodules) for submodule in submodules: if submodule[1].name != '__init__.py': - self.scan_module(submodule[0], submodule[1]) + self._scan_module(submodule[0], submodule[1]) except Exception: import traceback - self.results.warnings += ( - f"Error scanning '{subpath}': {traceback.format_exc()}\n") + 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: @@ -317,10 +334,9 @@ class DirectoryScan: # meta_lines is just anything containing '# ba_meta '; make sure # the ba_meta is in the right place. if mline[0] != 'ba_meta': - self.results.warnings += ( - 'Warning: ' + str(subpath) + - ': malformed ba_meta statement on line ' + - str(lindex + 1) + '.\n') + 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'): # Ignore 'require api X' lines in this pass. @@ -328,31 +344,28 @@ class DirectoryScan: elif len(mline) != 3 or mline[1] != 'export': # Currently we only support 'ba_meta export FOO'; # complain for anything else we see. - self.results.warnings += ( - 'Warning: ' + str(subpath) + - ': unrecognized ba_meta statement on line ' + - str(lindex + 1) + '.\n') + self.results.warnings.append( + f'Warning: {subpath}' + f': unrecognized ba_meta statement on line {lindex + 1}.') else: # Looks like we've got a valid export line! modulename = '.'.join(subpath.parts) if subpath.name.endswith('.py'): modulename = modulename[:-3] - exporttype = mline[2] + exporttypestr = mline[2] export_class_name = self._get_export_class_name( subpath, flines, lindex) if export_class_name is not None: classname = modulename + '.' + export_class_name - if exporttype == 'game': - self.results.games.append(classname) - elif exporttype == 'plugin': - self.results.plugins.append(classname) - elif exporttype == 'keyboard': - self.results.keyboards.append(classname) - else: - self.results.warnings += ( - 'Warning: ' + str(subpath) + - ': unrecognized export type "' + exporttype + - '" on line ' + str(lindex + 1) + '.\n') + + # If export type is one of our shortcuts, sub in the + # actual class path. Otherwise assume its a classpath + # itself. + exporttype = EXPORT_CLASS_NAME_SHORTCUTS.get(exporttypestr) + if exporttype is None: + exporttype = exporttypestr + self.results.exports.setdefault(exporttype, + []).append(classname) def _get_export_class_name(self, subpath: Path, lines: list[str], lindex: int) -> str | None: @@ -374,13 +387,12 @@ class DirectoryScan: classname = cbits[0] break # Success! if classname is None: - self.results.warnings += ( - 'Warning: ' + str(subpath) + ': class definition not found' - ' below "ba_meta export" statement on line ' + - str(lindexorig + 1) + '.\n') + self.results.warnings.append( + f'Warning: {subpath}: class definition not found below' + f' "ba_meta export" statement on line {lindexorig + 1}.') return classname - def get_api_requirement( + def _get_api_requirement( self, subpath: Path, meta_lines: dict[int, list[str]], @@ -401,15 +413,15 @@ class DirectoryScan: # Ok; not successful. lets issue warnings for a few error cases. if len(lines) > 1: - self.results.warnings += ( - 'Warning: ' + str(subpath) + - ': multiple "# ba_meta require api " lines found;' - ' ignoring module.\n') + self.results.warnings.append( + f'Warning: {subpath}: multiple' + ' "# ba_meta require api " lines found;' + ' 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 += ( - 'Warning: ' + str(subpath) + - ': no valid "# ba_meta require api " line found;' - ' ignoring module.\n') + self.results.warnings.append( + f'Warning: {subpath}:' + ' no valid "# ba_meta require api " line found;' + ' ignoring module.') return None diff --git a/dist/ba_data/python/ba/_multiteamsession.py b/dist/ba_data/python/ba/_multiteamsession.py index efc7a42..cfe4a7c 100644 --- a/dist/ba_data/python/ba/_multiteamsession.py +++ b/dist/ba_data/python/ba/_multiteamsession.py @@ -94,9 +94,11 @@ class MultiTeamSession(Session): playlist = _playlist.get_default_free_for_all_playlist() # Resolve types and whatnot to get our final playlist. - playlist_resolved = _playlist.filter_playlist(playlist, - sessiontype=type(self), - add_resolved_type=True) + playlist_resolved = _playlist.filter_playlist( + playlist, + sessiontype=type(self), + add_resolved_type=True, + name='default teams' if self.use_teams else 'default ffa') if not playlist_resolved: raise RuntimeError('Playlist contains no valid games.') diff --git a/dist/ba_data/python/ba/_net.py b/dist/ba_data/python/ba/_net.py index 003b032..7fac86f 100644 --- a/dist/ba_data/python/ba/_net.py +++ b/dist/ba_data/python/ba/_net.py @@ -134,15 +134,16 @@ class MasterServerCallThread(threading.Thread): import json from efro.error import is_urllib_communication_error - from ba import _general + from ba._general import Call, utf8_all + from ba._internal import get_master_server_address response_data: Any = None url: str | None = None try: - self._data = _general.utf8_all(self._data) + self._data = utf8_all(self._data) _ba.set_thread_name('BA_ServerCallThread') if self._request_type == 'get': - url = (_ba.get_master_server_address() + '/' + self._request + + url = (get_master_server_address() + '/' + self._request + '?' + urllib.parse.urlencode(self._data)) response = urllib.request.urlopen( urllib.request.Request( @@ -150,7 +151,7 @@ class MasterServerCallThread(threading.Thread): context=_ba.app.net.sslcontext, timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS) elif self._request_type == 'post': - url = _ba.get_master_server_address() + '/' + self._request + url = get_master_server_address() + '/' + self._request response = urllib.request.urlopen( urllib.request.Request( url, @@ -189,7 +190,7 @@ class MasterServerCallThread(threading.Thread): response_data = None if self._callback is not None: - _ba.pushcall(_general.Call(self._run_callback, response_data), + _ba.pushcall(Call(self._run_callback, response_data), from_other_thread=True) diff --git a/dist/ba_data/python/ba/_playlist.py b/dist/ba_data/python/ba/_playlist.py index b6e3604..5518601 100644 --- a/dist/ba_data/python/ba/_playlist.py +++ b/dist/ba_data/python/ba/_playlist.py @@ -5,6 +5,7 @@ from __future__ import annotations import copy +import logging from typing import Any, TYPE_CHECKING if TYPE_CHECKING: @@ -18,7 +19,8 @@ def filter_playlist(playlist: PlaylistType, sessiontype: type[_session.Session], add_resolved_type: bool = False, remove_unowned: bool = True, - mark_unowned: bool = False) -> PlaylistType: + 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 @@ -28,15 +30,15 @@ def filter_playlist(playlist: PlaylistType, # pylint: disable=too-many-locals # pylint: disable=too-many-branches # pylint: disable=too-many-statements - import _ba - from ba import _map - from ba import _general - from ba import _gameactivity + from ba._map import get_filtered_map_name + 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: - unowned_maps = _map.get_unowned_maps() - unowned_game_types = _ba.app.meta.get_unowned_game_types() + unowned_maps = get_unowned_maps() + unowned_game_types = get_unowned_game_types() else: unowned_maps = [] unowned_game_types = set() @@ -53,7 +55,7 @@ def filter_playlist(playlist: PlaylistType, del entry['map'] # Update old map names to new ones. - entry['settings']['map'] = _map.get_filtered_map_name( + entry['settings']['map'] = get_filtered_map_name( entry['settings']['map']) if remove_unowned and entry['settings']['map'] in unowned_maps: continue @@ -120,8 +122,7 @@ def filter_playlist(playlist: PlaylistType, entry['type'] = ( 'bastd.game.targetpractice.TargetPracticeGame') - gameclass = _general.getclass(entry['type'], - _gameactivity.GameActivity) + gameclass = getclass(entry['type'], GameActivity) if remove_unowned and gameclass in unowned_game_types: continue @@ -139,7 +140,8 @@ def filter_playlist(playlist: PlaylistType, entry['settings'][setting.name] = setting.default goodlist.append(entry) except ImportError as exc: - print(f'Import failed while scanning playlist: {exc}') + logging.warning('Import failed while scanning playlist \'%s\': %s', + name, exc) except Exception: from ba import _error _error.print_exception() diff --git a/dist/ba_data/python/ba/_plugin.py b/dist/ba_data/python/ba/_plugin.py index 9e7200f..d05cdbe 100644 --- a/dist/ba_data/python/ba/_plugin.py +++ b/dist/ba_data/python/ba/_plugin.py @@ -4,6 +4,7 @@ from __future__ import annotations +import logging from typing import TYPE_CHECKING from dataclasses import dataclass @@ -25,6 +26,46 @@ class PluginSubsystem: self.potential_plugins: list[ba.PotentialPlugin] = [] self.active_plugins: dict[str, ba.Plugin] = {} + def on_meta_scan_complete(self) -> None: + """Should be called when meta-scanning is complete.""" + from ba._language import Lstr + + plugs = _ba.app.plugins + config_changed = False + found_new = False + plugstates: dict[str, dict] = _ba.app.config.setdefault('Plugins', {}) + assert isinstance(plugstates, dict) + + results = _ba.app.meta.scanresults + assert results is not None + + # 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)) + 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. + # they can also disable them in settings so they never load. + plugstates[class_path] = {'enabled': True} + config_changed = True + found_new = True + + plugs.potential_plugins.sort(key=lambda p: p.class_path) + + # Note: these days we complete meta-scan and immediately activate + # 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.playsound(_ba.getsound('ding')) + + if config_changed: + _ba.app.config.commit() + def on_app_running(self) -> None: """Should be called when the app reaches the running state.""" # Load up our plugins and go ahead and call their on_app_running calls. @@ -69,10 +110,7 @@ class PluginSubsystem: from ba._language import Lstr # Note: the plugins we load is purely based on what's enabled - # in the app config. Our meta-scan gives us a list of available - # plugins, but that is only used to give the user a list of plugins - # that they can enable. (we wouldn't want to look at meta-scan here - # anyway because it may not be done yet at this point in the launch) + # 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() @@ -90,8 +128,7 @@ class PluginSubsystem: subs=[('${PLUGIN}', plugkey), ('${ERROR}', str(exc))]), color=(1, 0, 0)) - _ba.log(f"Error loading plugin class '{plugkey}': {exc}", - to_server=False) + logging.exception("Error loading plugin class '%s'", plugkey) continue try: plugin = cls() @@ -118,10 +155,8 @@ class PluginSubsystem: color=(1, 1, 0), ) plugnames = ', '.join(disappeared_plugs) - _ba.log( - f'{len(disappeared_plugs)} plugin(s) no longer found:' - f' {plugnames}.', - to_server=False) + 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() diff --git a/dist/ba_data/python/ba/_servermode.py b/dist/ba_data/python/ba/_servermode.py index 19aecb5..8c8ed98 100644 --- a/dist/ba_data/python/ba/_servermode.py +++ b/dist/ba_data/python/ba/_servermode.py @@ -5,6 +5,7 @@ from __future__ import annotations import sys import time +import logging from typing import TYPE_CHECKING from efro.terminal import Clr @@ -13,6 +14,8 @@ from bacommon.servermanager import (ServerCommand, StartServerModeCommand, ChatMessageCommand, ScreenMessageCommand, ClientListCommand, KickCommand) import _ba +from ba._internal import (add_transaction, run_transactions, + get_v1_account_state) from ba._generated.enums import TimeType from ba._freeforallsession import FreeForAllSession from ba._dualteamsession import DualTeamSession @@ -227,7 +230,7 @@ class ServerController: def _prepare_to_serve(self) -> None: """Run in a timer to do prep before beginning to serve.""" - signed_in = _ba.get_v1_account_state() == 'signed_in' + signed_in = get_v1_account_state() == 'signed_in' if not signed_in: # Signing in to the local server account should not take long; @@ -247,14 +250,14 @@ class ServerController: if not self._playlist_fetch_sent_request: print(f'{Clr.SBLU}Requesting shared-playlist' f' {self._config.playlist_code}...{Clr.RST}') - _ba.add_transaction( + add_transaction( { 'type': 'IMPORT_PLAYLIST', 'code': str(self._config.playlist_code), 'overwrite': True }, callback=self._on_playlist_fetch_response) - _ba.run_transactions() + run_transactions() self._playlist_fetch_sent_request = True if self._playlist_fetch_got_response: @@ -302,7 +305,7 @@ class ServerController: appcfg = app.config sessiontype = self._get_session_type() - if _ba.get_v1_account_state() != 'signed_in': + if get_v1_account_state() != 'signed_in': print('WARNING: launch_server_session() expects to run ' 'with a signed in server account') @@ -322,21 +325,21 @@ class ServerController: # Need to add this in a transaction instead of just setting # it directly or it will get overwritten by the master-server. - _ba.add_transaction({ + add_transaction({ 'type': 'ADD_PLAYLIST', 'playlistType': ptypename, 'playlistName': self._playlist_name, 'playlist': self._config.playlist_inline }) - _ba.run_transactions() + run_transactions() if self._first_run: curtimestr = time.strftime('%c') - _ba.log( + startupmsg = ( f'{Clr.BLD}{Clr.BLU}{_ba.appnameupper()} {app.version}' f' ({app.build_number})' - f' entering server-mode {curtimestr}{Clr.RST}', - to_server=False) + f' entering server-mode {curtimestr}{Clr.RST}') + logging.info(startupmsg) if sessiontype is FreeForAllSession: appcfg['Free-for-All Playlist Selection'] = self._playlist_name diff --git a/dist/ba_data/python/ba/_session.py b/dist/ba_data/python/ba/_session.py index 5413499..02309f4 100644 --- a/dist/ba_data/python/ba/_session.py +++ b/dist/ba_data/python/ba/_session.py @@ -615,6 +615,7 @@ class Session: def transitioning_out_activity_was_freed( self, can_show_ad_on_death: bool) -> None: """(internal)""" + # pylint: disable=cyclic-import from ba._apputils import garbage_collect # Since things should be generally still right now, it's a good time diff --git a/dist/ba_data/python/ba/_store.py b/dist/ba_data/python/ba/_store.py index 81a2e21..f56b0a8 100644 --- a/dist/ba_data/python/ba/_store.py +++ b/dist/ba_data/python/ba/_store.py @@ -7,6 +7,7 @@ from __future__ import annotations from typing import TYPE_CHECKING import _ba +from ba import _internal if TYPE_CHECKING: from typing import Any @@ -366,11 +367,11 @@ def get_store_layout() -> dict[str, list[dict[str, Any]]]: 'games.ninja_fight', 'games.meteor_shower', 'games.target_practice' ] }] - if _ba.get_v1_account_misc_read_val('xmas', False): + 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 _ba.get_v1_account_misc_read_val('easter', False): + if _internal.get_v1_account_misc_read_val('easter', False): store_layout['characters'].append({ 'title': 'store.holidaySpecialText', 'items': ['characters.bunny'] @@ -401,10 +402,10 @@ def get_clean_price(price_string: str) -> str: def get_available_purchase_count(tab: str | None = None) -> int: """(internal)""" try: - if _ba.get_v1_account_state() != 'signed_in': + if _internal.get_v1_account_state() != 'signed_in': return 0 count = 0 - our_tickets = _ba.get_v1_account_ticket_count() + our_tickets = _internal.get_v1_account_ticket_count() store_data = get_store_layout() if tab is not None: tabs = [(tab, store_data[tab])] @@ -425,11 +426,11 @@ 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 = _ba.get_v1_account_misc_read_val( + ticket_cost = _internal.get_v1_account_misc_read_val( 'price.' + item, None) if ticket_cost is not None: if (our_tickets >= ticket_cost - and not _ba.get_purchased(item)): + and not _internal.get_purchased(item)): count += 1 return count @@ -463,7 +464,7 @@ def get_available_sale_time(tab: str) -> int | None: # We start the timer once we get the duration from # the server. - start_duration = _ba.get_v1_account_misc_read_val( + start_duration = _internal.get_v1_account_misc_read_val( 'proSaleDurationMinutes', None) if start_duration is not None: app.pro_sale_start_time = int( @@ -489,12 +490,12 @@ def get_available_sale_time(tab: str) -> int | None: sale_times.append(val) # Now look for sales in this tab. - sales_raw = _ba.get_v1_account_misc_read_val('sales', {}) + sales_raw = _internal.get_v1_account_misc_read_val('sales', {}) store_layout = get_store_layout() for section in store_layout[tab]: for item in section['items']: if item in sales_raw: - if not _ba.get_purchased(item): + if not _internal.get_purchased(item): to_end = ((datetime.datetime.utcfromtimestamp( sales_raw[item]['e']) - datetime.datetime.utcnow()).total_seconds()) @@ -509,3 +510,35 @@ def get_available_sale_time(tab: str) -> int | None: from ba import _error _error.print_exception('error calcing sale time') return None + + +def get_unowned_maps() -> list[str]: + """Return the list of local maps not owned by the current account. + + Category: **Asset Functions** + """ + unowned_maps: set[str] = set() + if not _ba.app.headless_mode: + for map_section in get_store_layout()['maps']: + for mapitem in map_section['items']: + if not _internal.get_purchased(mapitem): + m_info = get_store_item(mapitem) + unowned_maps.add(m_info['map_type'].name) + return sorted(unowned_maps) + + +def get_unowned_game_types() -> set[type[ba.GameActivity]]: + """Return present game types not owned by the current account.""" + try: + unowned_games: set[type[ba.GameActivity]] = set() + if not _ba.app.headless_mode: + for section in get_store_layout()['minigames']: + for mname in section['items']: + if not _internal.get_purchased(mname): + m_info = get_store_item(mname) + unowned_games.add(m_info['gametype']) + return unowned_games + except Exception: + from ba import _error + _error.print_exception('error calcing un-owned games') + return set() diff --git a/dist/ba_data/python/ba/internal.py b/dist/ba_data/python/ba/internal.py index 95a7bfc..fdf9980 100644 --- a/dist/ba_data/python/ba/internal.py +++ b/dist/ba_data/python/ba/internal.py @@ -6,10 +6,35 @@ Classes and functions contained here, while technically 'public', may change or disappear without warning, so should be avoided (or used sparingly and defensively) in mods. """ +from __future__ import annotations -from ba._map import (get_unowned_maps, get_map_class, register_map, - preload_map_preview_media, get_map_display_string, - get_filtered_map_name) +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) + +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) @@ -34,27 +59,174 @@ from ba._playlist import (get_default_free_for_all_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_store_item, get_clean_price, get_unowned_maps, + get_unowned_game_types) from ba._tournament import get_tournament_prize_strings from ba._gameutils import get_trophy_string +from ba._internal import ( + get_v2_fleet, get_master_server_address, is_blessed, get_news_show, + game_service_has_leaderboard, report_achievement, submit_score, + tournament_query, power_ranking_query, restore_purchases, purchase, + get_purchases_state, get_purchased, get_price, in_game_purchase, + add_transaction, reset_achievements, get_public_login_id, + have_outstanding_transactions, run_transactions, + get_v1_account_misc_read_val, get_v1_account_misc_read_val_2, + get_v1_account_misc_val, get_v1_account_ticket_count, + get_v1_account_state_num, get_v1_account_state, + get_v1_account_display_string, get_v1_account_type, get_v1_account_name, + sign_out_v1, sign_in_v1, mark_config_dirty) + __all__ = [ - 'get_unowned_maps', 'get_map_class', 'register_map', - 'preload_map_preview_media', 'get_map_display_string', - 'get_filtered_map_name', 'commit_app_config', 'get_device_value', - 'get_input_map_hash', 'get_input_device_config', 'getclass', 'json_prep', - 'get_type_name', 'JoinActivity', 'ScoreScreenActivity', - 'is_browser_likely_available', 'get_remote_app_name', - 'should_submit_debug_info', 'run_gpu_benchmark', 'run_cpu_benchmark', - 'run_media_reload_benchmark', 'run_stress_test', 'getcampaign', - 'PlayerProfilesChangedMessage', 'DEFAULT_TEAM_COLORS', - 'DEFAULT_TEAM_NAMES', 'do_play_music', 'master_server_get', - 'master_server_post', 'get_ip_address_type', - 'DEFAULT_REQUEST_TIMEOUT_SECONDS', 'get_default_powerup_distribution', - 'get_player_profile_colors', 'get_player_profile_icon', - 'get_player_colors', 'get_next_tip', 'get_default_free_for_all_playlist', - 'get_default_teams_playlist', 'filter_playlist', 'get_available_sale_time', - 'get_available_purchase_count', 'get_store_item_name_translated', - 'get_store_item_display_size', 'get_store_layout', 'get_store_item', - 'get_clean_price', 'get_tournament_prize_strings', 'get_trophy_string' + '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', + # DIVIDER + 'get_unowned_maps', + 'get_unowned_game_types', + 'get_map_class', + 'register_map', + 'preload_map_preview_media', + 'get_map_display_string', + 'get_filtered_map_name', + 'commit_app_config', + 'get_device_value', + 'get_input_map_hash', + 'get_input_device_config', + 'getclass', + 'json_prep', + 'get_type_name', + 'JoinActivity', + 'ScoreScreenActivity', + 'is_browser_likely_available', + 'get_remote_app_name', + 'should_submit_debug_info', + 'run_gpu_benchmark', + 'run_cpu_benchmark', + 'run_media_reload_benchmark', + 'run_stress_test', + 'getcampaign', + 'PlayerProfilesChangedMessage', + 'DEFAULT_TEAM_COLORS', + 'DEFAULT_TEAM_NAMES', + 'do_play_music', + 'master_server_get', + 'master_server_post', + 'get_ip_address_type', + 'DEFAULT_REQUEST_TIMEOUT_SECONDS', + 'get_default_powerup_distribution', + 'get_player_profile_colors', + 'get_player_profile_icon', + 'get_player_colors', + 'get_next_tip', + 'get_default_free_for_all_playlist', + 'get_default_teams_playlist', + 'filter_playlist', + 'get_available_sale_time', + 'get_available_purchase_count', + 'get_store_item_name_translated', + 'get_store_item_display_size', + 'get_store_layout', + 'get_store_item', + 'get_clean_price', + 'get_tournament_prize_strings', + 'get_trophy_string', + 'get_v2_fleet', + 'get_master_server_address', + 'is_blessed', + 'get_news_show', + 'game_service_has_leaderboard', + 'report_achievement', + 'submit_score', + 'tournament_query', + 'power_ranking_query', + 'restore_purchases', + 'purchase', + 'get_purchases_state', + 'get_purchased', + 'get_price', + 'in_game_purchase', + 'add_transaction', + 'reset_achievements', + 'get_public_login_id', + 'have_outstanding_transactions', + 'run_transactions', + 'get_v1_account_misc_read_val', + 'get_v1_account_misc_read_val_2', + 'get_v1_account_misc_val', + 'get_v1_account_ticket_count', + 'get_v1_account_state_num', + 'get_v1_account_state', + 'get_v1_account_display_string', + 'get_v1_account_type', + 'get_v1_account_name', + 'sign_out_v1', + 'sign_in_v1', + 'mark_config_dirty', ] diff --git a/dist/ba_data/python/bacommon/bacloud.py b/dist/ba_data/python/bacommon/bacloud.py index 5ce38d3..2453aa8 100644 --- a/dist/ba_data/python/bacommon/bacloud.py +++ b/dist/ba_data/python/bacommon/bacloud.py @@ -14,7 +14,7 @@ if TYPE_CHECKING: # Version is sent to the master-server with all commands. Can be incremented # if we need to change behavior server-side to go along with client changes. -BACLOUD_VERSION = 7 +BACLOUD_VERSION = 8 @ioprepped diff --git a/dist/ba_data/python/bacommon/cloud.py b/dist/ba_data/python/bacommon/cloud.py index 34e9cca..91512f9 100644 --- a/dist/ba_data/python/bacommon/cloud.py +++ b/dist/ba_data/python/bacommon/cloud.py @@ -21,7 +21,7 @@ class LoginProxyRequestMessage(Message): """Request send to the cloud to ask for a login-proxy.""" @classmethod - def get_response_types(cls) -> list[type[Response]]: + def get_response_types(cls) -> list[type[Response] | None]: return [LoginProxyRequestResponse] @@ -48,7 +48,7 @@ class LoginProxyStateQueryMessage(Message): proxykey: Annotated[str, IOAttrs('k')] @classmethod - def get_response_types(cls) -> list[type[Response]]: + def get_response_types(cls) -> list[type[Response] | None]: return [LoginProxyStateQueryResponse] @@ -82,7 +82,7 @@ class PingMessage(Message): """Standard ping.""" @classmethod - def get_response_types(cls) -> list[type[Response]]: + def get_response_types(cls) -> list[type[Response] | None]: return [PingResponse] @@ -99,7 +99,7 @@ class TestMessage(Message): testfoo: Annotated[int, IOAttrs('f')] @classmethod - def get_response_types(cls) -> list[type[Response]]: + def get_response_types(cls) -> list[type[Response] | None]: return [TestResponse] @@ -130,7 +130,7 @@ class WorkspaceFetchMessage(Message): state: Annotated[WorkspaceFetchState, IOAttrs('s')] @classmethod - def get_response_types(cls) -> list[type[Response]]: + def get_response_types(cls) -> list[type[Response] | None]: return [WorkspaceFetchResponse] diff --git a/dist/ba_data/python/bastd/activity/coopjoin.py b/dist/ba_data/python/bastd/activity/coopjoin.py index 1d1db17..b8cc8af 100644 --- a/dist/ba_data/python/bastd/activity/coopjoin.py +++ b/dist/ba_data/python/bastd/activity/coopjoin.py @@ -6,12 +6,11 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba from ba.internal import JoinActivity if TYPE_CHECKING: - from typing import Any, Sequence + pass class CoopJoinActivity(JoinActivity): @@ -25,16 +24,6 @@ class CoopJoinActivity(JoinActivity): session = self.session assert isinstance(session, ba.CoopSession) - # Let's show a list of scores-to-beat for 1 player at least. - assert session.campaign is not None - level_name_full = (session.campaign.name + ':' + - session.campaign_level_name) - config_str = ('1p' + session.campaign.getlevel( - session.campaign_level_name).get_score_version_string().replace( - ' ', '_')) - _ba.get_scores_to_beat(level_name_full, config_str, - ba.WeakCall(self._on_got_scores_to_beat)) - def on_transition_in(self) -> None: from bastd.actor.controlsguide import ControlsGuide from bastd.actor.text import Text @@ -53,143 +42,61 @@ class CoopJoinActivity(JoinActivity): position=(0, -95)).autoretain() ControlsGuide(delay=1.0).autoretain() - def _on_got_scores_to_beat(self, - scores: list[dict[str, Any]] | None) -> None: - # pylint: disable=too-many-locals - # pylint: disable=too-many-statements - from efro.util import asserttype - from bastd.actor.text import Text + ba.pushcall(self._show_remaining_achievements) - # Sort by originating date so that the most recent is first. - if scores is not None: - scores.sort(reverse=True, - key=lambda score: asserttype(score['time'], int)) + def _show_remaining_achievements(self) -> None: + from bastd.actor.text import Text # We only show achievements and challenges for CoopGameActivities. session = self.session assert isinstance(session, ba.CoopSession) gameinstance = session.get_current_game_instance() - if isinstance(gameinstance, ba.CoopGameActivity): - score_type = gameinstance.get_score_type() - if scores is not None: - achievement_challenges = [ - a for a in scores if a['type'] == 'achievement_challenge' - ] - score_challenges = [ - a for a in scores if a['type'] == 'score_challenge' - ] - else: - achievement_challenges = score_challenges = [] + if not isinstance(gameinstance, ba.CoopGameActivity): + return - delay = 1.0 - vpos = -140.0 - spacing = 25 - delay_inc = 0.1 + delay = 1.0 + vpos = -140.0 - def _add_t( - text: str | ba.Lstr, - h_offs: float = 0.0, - scale: float = 1.0, - color: Sequence[float] = (1.0, 1.0, 1.0, 0.46) - ) -> None: - Text(text, - scale=scale * 0.76, - h_align=Text.HAlign.LEFT, + # 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) + 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) + 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, - transition=Text.Transition.FADE_IN, - transition_delay=delay, - color=color, - position=(60 + h_offs, vpos)).autoretain() - - if score_challenges: - _add_t(ba.Lstr(value='${A}:', - subs=[('${A}', - ba.Lstr(resource='scoreChallengesText')) - ]), - scale=1.1) - delay += delay_inc - vpos -= spacing - for chal in score_challenges: - _add_t(str(chal['value'] if score_type == 'points' else ba. - timestring(int(chal['value']) * 10, - timeformat=ba.TimeFormat.MILLISECONDS - ).evaluate()) + ' (1 player)', - h_offs=30, - color=(0.9, 0.7, 1.0, 0.8)) - delay += delay_inc - vpos -= 0.6 * spacing - _add_t(chal['player'], - h_offs=40, - color=(0.8, 1, 0.8, 0.6), - scale=0.8) - delay += delay_inc - vpos -= 1.2 * spacing - vpos -= 0.5 * spacing - - if achievement_challenges: - _add_t(ba.Lstr( - value='${A}:', - subs=[('${A}', - ba.Lstr(resource='achievementChallengesText'))]), - scale=1.1) - delay += delay_inc - vpos -= spacing - for chal in achievement_challenges: - _add_t(str(chal['value']), - h_offs=30, - color=(0.9, 0.7, 1.0, 0.8)) - delay += delay_inc - vpos -= 0.6 * spacing - _add_t(chal['player'], - h_offs=40, - color=(0.8, 1, 0.8, 0.6), - scale=0.8) - delay += delay_inc - vpos -= 1.2 * spacing - vpos -= 0.5 * spacing - - # 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) - 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) - 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'), + 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: + delay += 0.05 + 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 - 10, vpos), + position=(ts_h_offs + 15, vpos + 10), transition=Text.Transition.FADE_IN, - scale=1.1 * 0.76, + scale=0.7, 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: - delay += 0.05 - 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() + color=(1, 1, 1, 0.5), + transition_delay=delay + 0.5).autoretain() diff --git a/dist/ba_data/python/bastd/activity/coopscore.py b/dist/ba_data/python/bastd/activity/coopscore.py index dc7c06c..5e71b05 100644 --- a/dist/ba_data/python/bastd/activity/coopscore.py +++ b/dist/ba_data/python/bastd/activity/coopscore.py @@ -8,8 +8,8 @@ from __future__ import annotations import random from typing import TYPE_CHECKING -import _ba import ba +import ba.internal from bastd.actor.text import Text from bastd.actor.zoomtext import ZoomText @@ -52,9 +52,9 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): ba.app.ach.achievements_for_coop_level(self._campaign.name + ':' + settings['level'])) - self._account_type = (_ba.get_v1_account_type() - if _ba.get_v1_account_state() == 'signed_in' else - None) + self._account_type = (ba.internal.get_v1_account_type() + if ba.internal.get_v1_account_state() + == 'signed_in' else None) self._game_service_icon_color: Sequence[float] | None self._game_service_achievements_texture: ba.Texture | None @@ -167,7 +167,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): # If game-center/etc scores are available we show our friends' # scores. Otherwise we show our local high scores. - self._show_friend_scores = _ba.game_service_has_leaderboard( + self._show_friend_scores = ba.internal.game_service_has_leaderboard( self._game_name_str, self._game_config_str) try: @@ -264,12 +264,12 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): self.end({'outcome': 'next_level'}) def _ui_gc(self) -> None: - _ba.show_online_score_ui('leaderboard', - game=self._game_name_str, - game_version=self._game_config_str) + ba.internal.show_online_score_ui('leaderboard', + game=self._game_name_str, + game_version=self._game_config_str) def _ui_show_achievements(self) -> None: - _ba.show_online_score_ui('achievements') + ba.internal.show_online_score_ui('achievements') def _ui_worlds_best(self) -> None: if self._score_link is None: @@ -331,7 +331,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): # to the game (like on mac). can_select_extra_buttons = ba.app.platform == 'android' - _ba.set_ui_input_device(None) # Menu is up for grabs. + ba.internal.set_ui_input_device(None) # Menu is up for grabs. if self._show_friend_scores: ba.buttonwidget(parent=rootc, @@ -483,7 +483,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): timetype=ba.TimeType.REAL) def _update_corner_button_positions(self) -> None: - offs = -55 if _ba.is_party_icon_visible() else 0 + offs = -55 if ba.internal.is_party_icon_visible() else 0 assert self._corner_button_offs is not None pos_x = self._corner_button_offs[0] + offs pos_y = self._corner_button_offs[1] @@ -497,9 +497,9 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): # 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 + 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() + self._server_transitioning = ba.app.server.handle_transition() assert isinstance(self._server_transitioning, bool) # If server-mode is handling this, don't do anything ourself. @@ -528,7 +528,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): if ba.app.server is not None: # Host can't press retry button, so anyone can do it instead. time_till_assign = max( - 0, self._birth_time + self._min_view_time - _ba.time()) + 0, self._birth_time + self._min_view_time - ba.time()) ba.timer(time_till_assign, ba.WeakCall(self._safe_assign, player)) @@ -552,7 +552,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): # Any time we complete a level, set the next one as unlocked. if self._is_complete and self._is_more_levels: - _ba.add_transaction({ + ba.internal.add_transaction({ 'type': 'COMPLETE_LEVEL', 'campaign': self._campaign.name, 'level': self._level_name @@ -632,7 +632,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): if ba.app.server is None: # If we're running in normal non-headless build, show this text # because only host can continue the game. - adisp = _ba.get_v1_account_display_string() + adisp = ba.internal.get_v1_account_display_string() txt = Text(ba.Lstr(resource='waitingForHostText', subs=[('${HOST}', adisp)]), maxwidth=300, @@ -726,14 +726,14 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): if self._score is not None: sver = (self._campaign.getlevel( self._level_name).get_score_version_string()) - _ba.add_transaction({ + ba.internal.add_transaction({ 'type': 'SET_LEVEL_LOCAL_HIGH_SCORES', 'campaign': self._campaign.name, 'level': self._level_name, 'scoreVersion': sver, 'scores': our_high_scores_all }) - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': # We expect this only in kiosk mode; complain otherwise. if not (ba.app.demo_mode or ba.app.arcade_mode): print('got not-signed-in at score-submit; unexpected') @@ -743,21 +743,22 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): else: assert self._game_name_str is not None assert self._game_config_str is not None - _ba.submit_score(self._game_name_str, - self._game_config_str, - name_str, - self._score, - ba.WeakCall(self._got_score_results), - ba.WeakCall(self._got_friend_score_results) - if self._show_friend_scores else None, - order=self._score_order, - tournament_id=self.session.tournament_id, - score_type=self._score_type, - campaign=self._campaign.name, - level=self._level_name) + ba.internal.submit_score( + self._game_name_str, + self._game_config_str, + name_str, + self._score, + ba.WeakCall(self._got_score_results), + ba.WeakCall(self._got_friend_score_results) + if self._show_friend_scores else None, + order=self._score_order, + tournament_id=self.session.tournament_id, + score_type=self._score_type, + campaign=self._campaign.name, + level=self._level_name) # Apply the transactions we've been adding locally. - _ba.run_transactions() + ba.internal.run_transactions() # If we're not doing the world's-best button, just show a title # instead. @@ -1074,9 +1075,12 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): else: self._score_link = results['link'] assert self._score_link is not None - if not self._score_link.startswith('http://'): - self._score_link = (_ba.get_master_server_address() + '/' + - self._score_link) + # Prepend our master-server addr if its a relative addr. + if (not self._score_link.startswith('http://') + and not self._score_link.startswith('https://')): + self._score_link = ( + ba.internal.get_master_server_address() + '/' + + self._score_link) self._score_loading_status = None if 'tournamentSecondsRemaining' in results: secs_remaining = results['tournamentSecondsRemaining'] diff --git a/dist/ba_data/python/bastd/actor/controlsguide.py b/dist/ba_data/python/bastd/actor/controlsguide.py index 92a1115..40e82f6 100644 --- a/dist/ba_data/python/bastd/actor/controlsguide.py +++ b/dist/ba_data/python/bastd/actor/controlsguide.py @@ -6,8 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: from typing import Any, Sequence @@ -317,9 +317,8 @@ class ControlsGuide(ba.Actor): # an input device that is *not* the touchscreen. # (otherwise it is confusing to see the touchscreen buttons right # next to our display buttons) - touchscreen: ba.InputDevice | None = _ba.getinputdevice('TouchScreen', - '#1', - doraise=False) + touchscreen: ba.InputDevice | None = ba.internal.getinputdevice( + 'TouchScreen', '#1', doraise=False) if touchscreen is not None: # We look at the session's players; not the activity's. @@ -385,7 +384,7 @@ class ControlsGuide(ba.Actor): # If there's no players with input devices yet, try to default to # showing keyboard controls. if not input_devices: - kbd = _ba.getinputdevice('Keyboard', '#1', doraise=False) + kbd = ba.internal.getinputdevice('Keyboard', '#1', doraise=False) if kbd is not None: input_devices.append(kbd) diff --git a/dist/ba_data/python/bastd/actor/popuptext.py b/dist/ba_data/python/bastd/actor/popuptext.py index d078e7e..ab270dd 100644 --- a/dist/ba_data/python/bastd/actor/popuptext.py +++ b/dist/ba_data/python/bastd/actor/popuptext.py @@ -36,9 +36,9 @@ class PopupText(ba.Actor): if len(color) == 3: color = (color[0], color[1], color[2], 1.0) pos = (position[0] + offset[0] + random_offset * - (0.5 - random.random()), position[1] + offset[0] + + (0.5 - random.random()), position[1] + offset[1] + random_offset * (0.5 - random.random()), position[2] + - offset[0] + random_offset * (0.5 - random.random())) + offset[2] + random_offset * (0.5 - random.random())) self.node = ba.newnode('text', attrs={ diff --git a/dist/ba_data/python/bastd/actor/spaz.py b/dist/ba_data/python/bastd/actor/spaz.py index 5362c2f..d227789 100644 --- a/dist/ba_data/python/bastd/actor/spaz.py +++ b/dist/ba_data/python/bastd/actor/spaz.py @@ -81,7 +81,7 @@ class Spaz(ba.Actor): factory = SpazFactory.get() - # we need to behave slightly different in the tutorial + # We need to behave slightly different in the tutorial. self._demo_mode = demo_mode self.play_big_death_sound = False @@ -758,7 +758,7 @@ class Spaz(ba.Actor): tex = PowerupBoxFactory.get().tex_punch self._flash_billboard(tex) self.equip_boxing_gloves() - if self.powerups_expire: + if self.powerups_expire and not self.default_boxing_gloves: self.node.boxing_gloves_flashing = False self.node.mini_billboard_3_texture = tex t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) @@ -966,7 +966,7 @@ class Spaz(ba.Actor): self.on_punched(damage) # If damage was significant, lets show it. - if damage > 350: + if damage >= 350: assert msg.force_direction is not None ba.show_damage_count('-' + str(int(damage / 10)) + '%', msg.pos, msg.force_direction) @@ -977,11 +977,13 @@ class Spaz(ba.Actor): ba.playsound(SpazFactory.get().punch_sound_stronger, 1.0, position=self.node.position) - if damage > 500: + if damage >= 500: sounds = SpazFactory.get().punch_sound_strong sound = sounds[random.randrange(len(sounds))] - else: + elif damage >= 100: sound = SpazFactory.get().punch_sound + else: + sound = SpazFactory.get().punch_sound_weak ba.playsound(sound, 1.0, position=self.node.position) # Throw up some chunks. @@ -1075,7 +1077,7 @@ class Spaz(ba.Actor): # us if its grown high enough. if self.hitpoints <= 0: damage_avg = self.node.damage_smoothed * damage_scale - if damage_avg > 1000: + if damage_avg >= 1000: self.shatter() elif isinstance(msg, BombDiedMessage): @@ -1341,9 +1343,9 @@ class Spaz(ba.Actor): hit_type='impact')) self.node.handlemessage('knockout', max(0.0, 50.0 * intensity)) sounds: Sequence[ba.Sound] - if intensity > 5.0: + if intensity >= 5.0: sounds = SpazFactory.get().impact_sounds_harder - elif intensity > 3.0: + elif intensity >= 3.0: sounds = SpazFactory.get().impact_sounds_hard else: sounds = SpazFactory.get().impact_sounds_medium diff --git a/dist/ba_data/python/bastd/actor/spazappearance.py b/dist/ba_data/python/bastd/actor/spazappearance.py index bab2a8c..801000e 100644 --- a/dist/ba_data/python/bastd/actor/spazappearance.py +++ b/dist/ba_data/python/bastd/actor/spazappearance.py @@ -5,8 +5,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: pass @@ -16,66 +16,67 @@ def get_appearances(include_locked: bool = False) -> list[str]: """Get the list of available spaz appearances.""" # pylint: disable=too-many-statements # pylint: disable=too-many-branches + get_purchased = ba.internal.get_purchased disallowed = [] if not include_locked: # hmm yeah this'll be tough to hack... - if not _ba.get_purchased('characters.santa'): + if not get_purchased('characters.santa'): disallowed.append('Santa Claus') - if not _ba.get_purchased('characters.frosty'): + if not get_purchased('characters.frosty'): disallowed.append('Frosty') - if not _ba.get_purchased('characters.bones'): + if not get_purchased('characters.bones'): disallowed.append('Bones') - if not _ba.get_purchased('characters.bernard'): + if not get_purchased('characters.bernard'): disallowed.append('Bernard') - if not _ba.get_purchased('characters.pixie'): + if not get_purchased('characters.pixie'): disallowed.append('Pixel') - if not _ba.get_purchased('characters.pascal'): + if not get_purchased('characters.pascal'): disallowed.append('Pascal') - if not _ba.get_purchased('characters.actionhero'): + if not get_purchased('characters.actionhero'): disallowed.append('Todd McBurton') - if not _ba.get_purchased('characters.taobaomascot'): + if not get_purchased('characters.taobaomascot'): disallowed.append('Taobao Mascot') - if not _ba.get_purchased('characters.agent'): + if not get_purchased('characters.agent'): disallowed.append('Agent Johnson') - if not _ba.get_purchased('characters.jumpsuit'): + if not get_purchased('characters.jumpsuit'): disallowed.append('Lee') - if not _ba.get_purchased('characters.assassin'): + if not get_purchased('characters.assassin'): disallowed.append('Zola') - if not _ba.get_purchased('characters.wizard'): + if not get_purchased('characters.wizard'): disallowed.append('Grumbledorf') - if not _ba.get_purchased('characters.cowboy'): + if not get_purchased('characters.cowboy'): disallowed.append('Butch') - if not _ba.get_purchased('characters.witch'): + if not get_purchased('characters.witch'): disallowed.append('Witch') - if not _ba.get_purchased('characters.warrior'): + if not get_purchased('characters.warrior'): disallowed.append('Warrior') - if not _ba.get_purchased('characters.superhero'): + if not get_purchased('characters.superhero'): disallowed.append('Middle-Man') - if not _ba.get_purchased('characters.alien'): + if not get_purchased('characters.alien'): disallowed.append('Alien') - if not _ba.get_purchased('characters.oldlady'): + if not get_purchased('characters.oldlady'): disallowed.append('OldLady') - if not _ba.get_purchased('characters.gladiator'): + if not get_purchased('characters.gladiator'): disallowed.append('Gladiator') - if not _ba.get_purchased('characters.wrestler'): + if not get_purchased('characters.wrestler'): disallowed.append('Wrestler') - if not _ba.get_purchased('characters.operasinger'): + if not get_purchased('characters.operasinger'): disallowed.append('Gretel') - if not _ba.get_purchased('characters.robot'): + if not get_purchased('characters.robot'): disallowed.append('Robot') - if not _ba.get_purchased('characters.cyborg'): + if not get_purchased('characters.cyborg'): disallowed.append('B-9000') - if not _ba.get_purchased('characters.bunny'): + if not get_purchased('characters.bunny'): disallowed.append('Easter Bunny') - if not _ba.get_purchased('characters.kronk'): + if not get_purchased('characters.kronk'): disallowed.append('Kronk') - if not _ba.get_purchased('characters.zoe'): + if not get_purchased('characters.zoe'): disallowed.append('Zoe') - if not _ba.get_purchased('characters.jackmorgan'): + if not get_purchased('characters.jackmorgan'): disallowed.append('Jack Morgan') - if not _ba.get_purchased('characters.mel'): + if not get_purchased('characters.mel'): disallowed.append('Mel') - if not _ba.get_purchased('characters.snakeshadow'): + if not get_purchased('characters.snakeshadow'): disallowed.append('Snake Shadow') return [ s for s in list(ba.app.spaz_appearances.keys()) if s not in disallowed diff --git a/dist/ba_data/python/bastd/actor/spazfactory.py b/dist/ba_data/python/bastd/actor/spazfactory.py index 0840d8f..2611d1c 100644 --- a/dist/ba_data/python/bastd/actor/spazfactory.py +++ b/dist/ba_data/python/bastd/actor/spazfactory.py @@ -7,8 +7,8 @@ from __future__ import annotations from typing import TYPE_CHECKING import ba +import ba.internal from bastd.gameutils import SharedObjects -import _ba if TYPE_CHECKING: from typing import Any, Sequence @@ -38,6 +38,9 @@ class SpazFactory: """The sound that plays for an 'important' spaz death such as in co-op games.""" + punch_sound_weak: ba.Sound + """A weak punch ba.Sound.""" + punch_sound: ba.Sound """A standard punch ba.Sound.""" @@ -98,6 +101,7 @@ class SpazFactory: self.impact_sounds_harder = (ba.getsound('bigImpact'), ba.getsound('bigImpact2')) self.single_player_death_sound = ba.getsound('playerDeath') + self.punch_sound_weak = ba.getsound('punchWeak01') self.punch_sound = ba.getsound('punch01') self.punch_sound_strong = (ba.getsound('punchStrong01'), ba.getsound('punchStrong02')) @@ -208,15 +212,18 @@ class SpazFactory: # Lets load some basic rules. # (allows them to be tweaked from the master server) - self.shield_decay_rate = _ba.get_v1_account_misc_read_val('rsdr', 10.0) - self.punch_cooldown = _ba.get_v1_account_misc_read_val('rpc', 400) - self.punch_cooldown_gloves = (_ba.get_v1_account_misc_read_val( + self.shield_decay_rate = ba.internal.get_v1_account_misc_read_val( + 'rsdr', 10.0) + self.punch_cooldown = ba.internal.get_v1_account_misc_read_val( + 'rpc', 400) + self.punch_cooldown_gloves = (ba.internal.get_v1_account_misc_read_val( 'rpcg', 300)) - self.punch_power_scale = _ba.get_v1_account_misc_read_val('rpp', 1.2) - self.punch_power_scale_gloves = (_ba.get_v1_account_misc_read_val( - 'rppg', 1.4)) - self.max_shield_spillover_damage = (_ba.get_v1_account_misc_read_val( - 'rsms', 500)) + self.punch_power_scale = ba.internal.get_v1_account_misc_read_val( + 'rpp', 1.2) + self.punch_power_scale_gloves = ( + ba.internal.get_v1_account_misc_read_val('rppg', 1.4)) + self.max_shield_spillover_damage = ( + ba.internal.get_v1_account_misc_read_val('rsms', 500)) def get_style(self, character: str) -> str: """Return the named style for this character. diff --git a/dist/ba_data/python/bastd/game/easteregghunt.py b/dist/ba_data/python/bastd/game/easteregghunt.py index 3372522..1dc4de0 100644 --- a/dist/ba_data/python/bastd/game/easteregghunt.py +++ b/dist/ba_data/python/bastd/game/easteregghunt.py @@ -44,7 +44,10 @@ class EasterEggHuntGame(ba.TeamGameActivity[Player, Team]): name = 'Easter Egg Hunt' description = 'Gather eggs!' - available_settings = [ba.BoolSetting('Pro Mode', default=False)] + available_settings = [ + ba.BoolSetting('Pro Mode', default=False), + ba.BoolSetting('Epic Mode', default=False), + ] scoreconfig = ba.ScoreConfig(label='Score', scoretype=ba.ScoreType.POINTS) # We're currently hard-coded for one map. @@ -70,6 +73,7 @@ class EasterEggHuntGame(ba.TeamGameActivity[Player, Team]): self.egg_tex_3 = ba.gettexture('eggTex3') self._collect_sound = ba.getsound('powerup01') self._pro_mode = settings.get('Pro Mode', False) + self._epic_mode = settings.get('Epic Mode', False) self._max_eggs = 1.0 self.egg_material = ba.Material() self.egg_material.add_actions( @@ -81,7 +85,9 @@ class EasterEggHuntGame(ba.TeamGameActivity[Player, Team]): self._bots: SpazBotSet | None = None # Base class overrides - self.default_music = ba.MusicType.FORWARD_MARCH + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC if self._epic_mode else + ba.MusicType.FORWARD_MARCH) def on_team_join(self, team: Team) -> None: if self.has_begun(): diff --git a/dist/ba_data/python/bastd/game/football.py b/dist/ba_data/python/bastd/game/football.py index 2bf0bd9..2183fa9 100644 --- a/dist/ba_data/python/bastd/game/football.py +++ b/dist/ba_data/python/bastd/game/football.py @@ -106,8 +106,8 @@ class FootballTeamGame(ba.TeamGameActivity[Player, Team]): ], default=1.0, ), + ba.BoolSetting('Epic Mode', default=False), ] - default_music = ba.MusicType.FOOTBALL @classmethod def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: @@ -143,6 +143,10 @@ class FootballTeamGame(ba.TeamGameActivity[Player, Team]): self._flag_respawn_light: ba.NodeActor | None = None self._score_to_win = int(settings['Score to Win']) self._time_limit = float(settings['Time Limit']) + self._epic_mode = bool(settings['Epic Mode']) + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC + if self._epic_mode else ba.MusicType.FOOTBALL) def get_instance_description(self) -> str | Sequence: touchdowns = self._score_to_win / 7 @@ -330,6 +334,7 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): tips = ['Use the pick-up button to grab the flag < ${PICKUP} >'] scoreconfig = ba.ScoreConfig(scoretype=ba.ScoreType.MILLISECONDS, version='B') + default_music = ba.MusicType.FOOTBALL # FIXME: Need to update co-op games to use getscoreconfig. diff --git a/dist/ba_data/python/bastd/game/hockey.py b/dist/ba_data/python/bastd/game/hockey.py index 97213a6..11110e7 100644 --- a/dist/ba_data/python/bastd/game/hockey.py +++ b/dist/ba_data/python/bastd/game/hockey.py @@ -137,8 +137,8 @@ class HockeyGame(ba.TeamGameActivity[Player, Team]): ], default=1.0, ), + ba.BoolSetting('Epic Mode', default=False), ] - default_music = ba.MusicType.HOCKEY @classmethod def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: @@ -203,6 +203,10 @@ class HockeyGame(ba.TeamGameActivity[Player, Team]): self._puck: Puck | None = None self._score_to_win = int(settings['Score to Win']) self._time_limit = float(settings['Time Limit']) + self._epic_mode = bool(settings['Epic Mode']) + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC + if self._epic_mode else ba.MusicType.HOCKEY) def get_instance_description(self) -> str | Sequence: if self._score_to_win == 1: diff --git a/dist/ba_data/python/bastd/game/keepaway.py b/dist/ba_data/python/bastd/game/keepaway.py index 0dd355e..3d3477e 100644 --- a/dist/ba_data/python/bastd/game/keepaway.py +++ b/dist/ba_data/python/bastd/game/keepaway.py @@ -76,9 +76,9 @@ class KeepAwayGame(ba.TeamGameActivity[Player, Team]): ], default=1.0, ), + ba.BoolSetting('Epic Mode', default=False), ] scoreconfig = ba.ScoreConfig(label='Time Held') - default_music = ba.MusicType.KEEP_AWAY @classmethod def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: @@ -115,6 +115,10 @@ class KeepAwayGame(ba.TeamGameActivity[Player, Team]): self._flag: Flag | None = None self._hold_time = int(settings['Hold Time']) self._time_limit = float(settings['Time Limit']) + self._epic_mode = bool(settings['Epic Mode']) + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC + if self._epic_mode else ba.MusicType.KEEP_AWAY) def get_instance_description(self) -> str | Sequence: return 'Carry the flag for ${ARG1} seconds.', self._hold_time diff --git a/dist/ba_data/python/bastd/game/kingofthehill.py b/dist/ba_data/python/bastd/game/kingofthehill.py index becdc8b..677c9fd 100644 --- a/dist/ba_data/python/bastd/game/kingofthehill.py +++ b/dist/ba_data/python/bastd/game/kingofthehill.py @@ -79,6 +79,7 @@ class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]): ], default=1.0, ), + ba.BoolSetting('Epic Mode', default=False), ] scoreconfig = ba.ScoreConfig(label='Time Held') @@ -115,6 +116,7 @@ class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]): self._scoring_team: weakref.ref[Team] | None = None self._hold_time = int(settings['Hold Time']) self._time_limit = float(settings['Time Limit']) + self._epic_mode = bool(settings['Epic Mode']) self._flag_region_material = ba.Material() self._flag_region_material.add_actions( conditions=('they_have_material', shared.player_material), @@ -128,7 +130,9 @@ class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]): )) # Base class overrides. - self.default_music = ba.MusicType.SCARY + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC + if self._epic_mode else ba.MusicType.SCARY) def get_instance_description(self) -> str | Sequence: return 'Secure the flag for ${ARG1} seconds.', self._hold_time diff --git a/dist/ba_data/python/bastd/mainmenu.py b/dist/ba_data/python/bastd/mainmenu.py index d5afa32..ed695c0 100644 --- a/dist/ba_data/python/bastd/mainmenu.py +++ b/dist/ba_data/python/bastd/mainmenu.py @@ -10,7 +10,7 @@ import weakref from typing import TYPE_CHECKING import ba -import _ba +import ba.internal if TYPE_CHECKING: from typing import Any @@ -67,7 +67,8 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): # host is navigating menus while they're just staring at an # empty-ish screen. tval = ba.Lstr(resource='hostIsNavigatingMenusText', - subs=[('${HOST}', _ba.get_v1_account_display_string())]) + subs=[('${HOST}', + ba.internal.get_v1_account_display_string())]) self._host_is_navigating_text = ba.NodeActor( ba.newnode('text', attrs={ @@ -251,7 +252,7 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): self._update() # Hopefully this won't hitch but lets space these out anyway. - _ba.add_clean_frame_callback(ba.WeakCall(self._start_preloads)) + ba.internal.add_clean_frame_callback(ba.WeakCall(self._start_preloads)) random.seed() @@ -274,7 +275,7 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): # We now want to wait until we're signed in before fetching news. def _try_fetching_news(self) -> None: - if _ba.get_v1_account_state() == 'signed_in': + if ba.internal.get_v1_account_state() == 'signed_in': self._fetch_news() self._fetch_timer = None @@ -282,7 +283,7 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): ba.app.main_menu_last_news_fetch_time = time.time() # UPDATE - We now just pull news from MRVs. - news = _ba.get_v1_account_misc_read_val('n', None) + news = ba.internal.get_v1_account_misc_read_val('n', None) if news is not None: self._got_news(news) @@ -453,6 +454,11 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): ba.app.ui.set_main_menu_window( CoopBrowserWindow( transition=None).get_root_widget()) + elif main_menu_location == 'Benchmarks & Stress Tests': + # pylint: disable=cyclic-import + from bastd.ui.debug import DebugWindow + ba.app.ui.set_main_menu_window( + DebugWindow(transition=None).get_root_widget()) else: # pylint: disable=cyclic-import from bastd.ui.mainmenu import MainMenuWindow @@ -757,7 +763,7 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): }) def _get_custom_logo_tex_name(self) -> str | None: - if _ba.get_v1_account_misc_read_val('easter', False): + if ba.internal.get_v1_account_misc_read_val('easter', False): return 'logoEaster' return None @@ -930,7 +936,7 @@ class MainMenuSession(ba.Session): def on_activity_end(self, activity: ba.Activity, results: Any) -> None: if self._locked: - _ba.unlock_all_input() + ba.internal.unlock_all_input() # Any ending activity leads us into the main menu one. self.setactivity(ba.newactivity(MainMenuActivity)) diff --git a/dist/ba_data/python/bastd/tutorial.py b/dist/ba_data/python/bastd/tutorial.py index 0cb2c7b..c92aed8 100644 --- a/dist/ba_data/python/bastd/tutorial.py +++ b/dist/ba_data/python/bastd/tutorial.py @@ -18,8 +18,8 @@ from __future__ import annotations import math from typing import TYPE_CHECKING -import _ba import ba +import ba.internal from bastd.actor import spaz as basespaz if TYPE_CHECKING: @@ -235,7 +235,7 @@ class TutorialActivity(ba.Activity[Player, Team]): super().on_begin() ba.set_analytics_screen('Tutorial Start') - _ba.increment_analytics_count('Tutorial start') + ba.internal.increment_analytics_count('Tutorial start') if bool(False): # Buttons on top. @@ -461,7 +461,7 @@ class TutorialActivity(ba.Activity[Player, Team]): def run(self, a: TutorialActivity) -> None: print('setting to', self._speed) - _ba.set_debug_speed_exponent(self._speed) + ba.internal.set_debug_speed_exponent(self._speed) class RemoveGloves: @@ -609,7 +609,7 @@ class TutorialActivity(ba.Activity[Player, Team]): pass def run(self, a: TutorialActivity) -> None: - _ba.increment_analytics_count('Tutorial finish') + ba.internal.increment_analytics_count('Tutorial finish') a.end() class Move: @@ -2328,7 +2328,7 @@ class TutorialActivity(ba.Activity[Player, Team]): ('${TOTAL}', str(len(self.players)))]) if count > 0 else '' if (count >= len(self.players) and self.players and not self._have_skipped): - _ba.increment_analytics_count('Tutorial skip') + ba.internal.increment_analytics_count('Tutorial skip') ba.set_analytics_screen('Tutorial Skip') self._have_skipped = True ba.playsound(ba.getsound('swish')) diff --git a/dist/ba_data/python/bastd/ui/account/__init__.py b/dist/ba_data/python/bastd/ui/account/__init__.py index 1ce8461..24e4007 100644 --- a/dist/ba_data/python/bastd/ui/account/__init__.py +++ b/dist/ba_data/python/bastd/ui/account/__init__.py @@ -4,7 +4,6 @@ from __future__ import annotations -import _ba import ba @@ -12,10 +11,11 @@ def show_sign_in_prompt(account_type: str | None = None) -> None: """Bring up a prompt telling the user they must sign in.""" from bastd.ui.confirm import ConfirmWindow from bastd.ui.account import settings + from ba.internal import sign_in_v1 if account_type == 'Google Play': ConfirmWindow( ba.Lstr(resource='notSignedInGooglePlayErrorText'), - lambda: _ba.sign_in_v1('Google Play'), + lambda: sign_in_v1('Google Play'), ok_text=ba.Lstr(resource='accountSettingsWindow.signInText'), width=460, height=130) diff --git a/dist/ba_data/python/bastd/ui/account/link.py b/dist/ba_data/python/bastd/ui/account/link.py index f92c9da..b16f839 100644 --- a/dist/ba_data/python/bastd/ui/account/link.py +++ b/dist/ba_data/python/bastd/ui/account/link.py @@ -8,8 +8,8 @@ import copy import time from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: from typing import Any @@ -50,7 +50,8 @@ class AccountLinkWindow(ba.Window): autoselect=True, icon=ba.gettexture('crossOut'), iconscale=1.2) - maxlinks = _ba.get_v1_account_misc_read_val('maxLinkAccounts', 5) + maxlinks = ba.internal.get_v1_account_misc_read_val( + 'maxLinkAccounts', 5) ba.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height * 0.56), @@ -84,17 +85,17 @@ class AccountLinkWindow(ba.Window): def _generate_press(self) -> None: from bastd.ui import account - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() return ba.screenmessage( ba.Lstr(resource='gatherWindow.requestingAPromoCodeText'), color=(0, 1, 0)) - _ba.add_transaction({ + ba.internal.add_transaction({ 'type': 'ACCOUNT_LINK_CODE_REQUEST', 'expire_time': time.time() + 5 }) - _ba.run_transactions() + ba.internal.run_transactions() def _enter_code_press(self) -> None: from bastd.ui import promocode diff --git a/dist/ba_data/python/bastd/ui/account/settings.py b/dist/ba_data/python/bastd/ui/account/settings.py index cdfb1c7..a75eb0f 100644 --- a/dist/ba_data/python/bastd/ui/account/settings.py +++ b/dist/ba_data/python/bastd/ui/account/settings.py @@ -8,8 +8,8 @@ from __future__ import annotations import time from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: pass @@ -25,7 +25,6 @@ class AccountSettingsWindow(ba.Window): close_once_signed_in: bool = False): # pylint: disable=too-many-statements - self._sign_in_game_circle_button: ba.Widget | None = None self._sign_in_v2_button: ba.Widget | None = None self._sign_in_device_button: ba.Widget | None = None @@ -45,10 +44,10 @@ class AccountSettingsWindow(ba.Window): self._r = 'accountSettingsWindow' self._modal = modal self._needs_refresh = False - self._signed_in = (_ba.get_v1_account_state() == 'signed_in') - self._account_state_num = _ba.get_v1_account_state_num() + self._signed_in = (ba.internal.get_v1_account_state() == 'signed_in') + self._account_state_num = ba.internal.get_v1_account_state_num() self._show_linked = (self._signed_in - and _ba.get_v1_account_misc_read_val( + and ba.internal.get_v1_account_misc_read_val( 'allowAccountLinking2', False)) self._check_sign_in_timer = ba.Timer(1.0, ba.WeakCall(self._update), @@ -58,7 +57,7 @@ class AccountSettingsWindow(ba.Window): # Currently we can only reset achievements on game-center. account_type: str | None if self._signed_in: - account_type = _ba.get_v1_account_type() + account_type = ba.internal.get_v1_account_type() else: account_type = None self._can_reset_achievements = (account_type == 'Game Center') @@ -84,9 +83,6 @@ class AccountSettingsWindow(ba.Window): if app.platform == 'android' and app.subplatform == 'google': self._show_sign_in_buttons.append('Google Play') - elif app.platform == 'android' and app.subplatform == 'amazon': - self._show_sign_in_buttons.append('Game Circle') - # Local accounts are generally always available with a few key # exceptions. self._show_sign_in_buttons.append('Local') @@ -159,11 +155,12 @@ class AccountSettingsWindow(ba.Window): # Hmm should update this to use get_account_state_num. # Theoretically if we switch from one signed-in account to another # in the background this would break. - account_state_num = _ba.get_v1_account_state_num() - account_state = _ba.get_v1_account_state() + account_state_num = ba.internal.get_v1_account_state_num() + account_state = ba.internal.get_v1_account_state() - show_linked = (self._signed_in and _ba.get_v1_account_misc_read_val( - 'allowAccountLinking2', False)) + show_linked = (self._signed_in + and ba.internal.get_v1_account_misc_read_val( + 'allowAccountLinking2', False)) if (account_state_num != self._account_state_num or self._show_linked != show_linked or self._needs_refresh): @@ -191,8 +188,8 @@ class AccountSettingsWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui import confirm - account_state = _ba.get_v1_account_state() - account_type = (_ba.get_v1_account_type() + account_state = ba.internal.get_v1_account_state() + account_type = (ba.internal.get_v1_account_type() if account_state == 'signed_in' else 'unknown') is_google = account_type == 'Google Play' @@ -212,27 +209,24 @@ class AccountSettingsWindow(ba.Window): show_google_play_sign_in_button = (account_state == 'signed_out' and 'Google Play' in self._show_sign_in_buttons) - show_game_circle_sign_in_button = (account_state == 'signed_out' - and 'Game Circle' - in self._show_sign_in_buttons) show_device_sign_in_button = (account_state == 'signed_out' and 'Local' in self._show_sign_in_buttons) show_v2_sign_in_button = (account_state == 'signed_out' and 'V2' in self._show_sign_in_buttons) sign_in_button_space = 70.0 - show_game_service_button = (self._signed_in and account_type - in ['Game Center', 'Game Circle']) + show_game_service_button = (self._signed_in + and account_type in ['Game Center']) game_service_button_space = 60.0 - show_linked_accounts_text = (self._signed_in - and _ba.get_v1_account_misc_read_val( + show_linked_accounts_text = (self._signed_in and + ba.internal.get_v1_account_misc_read_val( 'allowAccountLinking2', False)) linked_accounts_text_space = 60.0 - show_achievements_button = ( - self._signed_in - and account_type in ('Google Play', 'Alibaba', 'Local', 'OUYA')) + show_achievements_button = (self._signed_in and account_type + in ('Google Play', 'Alibaba', 'Local', + 'OUYA', 'V2')) achievements_button_space = 60.0 show_achievements_text = (self._signed_in @@ -251,11 +245,17 @@ class AccountSettingsWindow(ba.Window): show_reset_progress_button = False reset_progress_button_space = 70.0 - show_player_profiles_button = self._signed_in - player_profiles_button_space = 100.0 + show_manage_v2_account_button = (self._signed_in + and account_type == 'V2' + and bool(False)) # Disabled for now. + manage_v2_account_button_space = 100.0 - show_link_accounts_button = (self._signed_in - and _ba.get_v1_account_misc_read_val( + show_player_profiles_button = self._signed_in + player_profiles_button_space = (70.0 if show_manage_v2_account_button + else 100.0) + + show_link_accounts_button = (self._signed_in and + ba.internal.get_v1_account_misc_read_val( 'allowAccountLinking2', False)) link_accounts_button_space = 70.0 @@ -282,8 +282,6 @@ class AccountSettingsWindow(ba.Window): self._sub_height += signing_in_text_space if show_google_play_sign_in_button: self._sub_height += sign_in_button_space - if show_game_circle_sign_in_button: - self._sub_height += sign_in_button_space if show_device_sign_in_button: self._sub_height += sign_in_button_space if show_v2_sign_in_button: @@ -306,6 +304,8 @@ class AccountSettingsWindow(ba.Window): self._sub_height += sign_in_benefits_space if show_reset_progress_button: self._sub_height += reset_progress_button_space + if show_manage_v2_account_button: + self._sub_height += manage_v2_account_button_space if show_player_profiles_button: self._sub_height += player_profiles_button_space if show_link_accounts_button: @@ -335,7 +335,8 @@ class AccountSettingsWindow(ba.Window): size=(0, 0), text=ba.Lstr( resource='accountSettingsWindow.deviceSpecificAccountText', - subs=[('${NAME}', _ba.get_v1_account_display_string())]), + subs=[('${NAME}', + ba.internal.get_v1_account_display_string())]), scale=0.7, color=(0.5, 0.5, 0.6), maxwidth=self._sub_width * 0.9, @@ -376,7 +377,7 @@ class AccountSettingsWindow(ba.Window): self._account_name_text = None if self._back_button is None: - bbtn = _ba.get_special_widget('back_button') + bbtn = ba.internal.get_special_widget('back_button') else: bbtn = self._back_button @@ -444,32 +445,8 @@ class AccountSettingsWindow(ba.Window): first_selectable = btn if ba.app.ui.use_toolbars: ba.widget(edit=btn, - right_widget=_ba.get_special_widget('party_button')) - ba.widget(edit=btn, left_widget=bbtn) - ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) - self._sign_in_text = None - - if show_game_circle_sign_in_button: - button_width = 350 - v -= sign_in_button_space - self._sign_in_game_circle_button = btn = ba.buttonwidget( - parent=self._subcontainer, - position=((self._sub_width - button_width) * 0.5, v - 20), - autoselect=True, - size=(button_width, 60), - label=ba.Lstr(value='${A}${B}', - subs=[('${A}', - ba.charstr( - ba.SpecialChar.GAME_CIRCLE_LOGO)), - ('${B}', - ba.Lstr(resource=self._r + - '.signInWithGameCircleText'))]), - on_activate_call=lambda: self._sign_in_press('Game Circle')) - if first_selectable is None: - first_selectable = btn - if ba.app.ui.use_toolbars: - ba.widget(edit=btn, - right_widget=_ba.get_special_widget('party_button')) + right_widget=ba.internal.get_special_widget( + 'party_button')) ba.widget(edit=btn, left_widget=bbtn) ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) self._sign_in_text = None @@ -514,7 +491,8 @@ class AccountSettingsWindow(ba.Window): first_selectable = btn if ba.app.ui.use_toolbars: ba.widget(edit=btn, - right_widget=_ba.get_special_widget('party_button')) + right_widget=ba.internal.get_special_widget( + 'party_button')) ba.widget(edit=btn, left_widget=bbtn) ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) self._sign_in_text = None @@ -560,11 +538,34 @@ class AccountSettingsWindow(ba.Window): first_selectable = btn if ba.app.ui.use_toolbars: ba.widget(edit=btn, - right_widget=_ba.get_special_widget('party_button')) + right_widget=ba.internal.get_special_widget( + 'party_button')) ba.widget(edit=btn, left_widget=bbtn) ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) self._sign_in_text = None + if show_manage_v2_account_button: + button_width = 300 + v -= manage_v2_account_button_space + self._manage_v2_button = btn = ba.buttonwidget( + parent=self._subcontainer, + position=((self._sub_width - button_width) * 0.5, v + 30), + autoselect=True, + size=(button_width, 60), + label=ba.Lstr(resource=self._r + '.manageAccount'), + color=(0.55, 0.5, 0.6), + icon=ba.gettexture('settingsIcon'), + textcolor=(0.75, 0.7, 0.8), + on_activate_call=lambda: ba.open_url( + 'https://ballistica.net/accountsettings')) + if first_selectable is None: + first_selectable = btn + if ba.app.ui.use_toolbars: + ba.widget(edit=btn, + right_widget=ba.internal.get_special_widget( + 'party_button')) + ba.widget(edit=btn, left_widget=bbtn) + if show_player_profiles_button: button_width = 300 v -= player_profiles_button_space @@ -582,18 +583,17 @@ class AccountSettingsWindow(ba.Window): first_selectable = btn if ba.app.ui.use_toolbars: ba.widget(edit=btn, - right_widget=_ba.get_special_widget('party_button')) + right_widget=ba.internal.get_special_widget( + 'party_button')) ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=0) # the button to go to OS-Specific leaderboards/high-score-lists/etc. if show_game_service_button: button_width = 300 v -= game_service_button_space * 0.85 - account_type = _ba.get_v1_account_type() + account_type = ba.internal.get_v1_account_type() if account_type == 'Game Center': account_type_name = ba.Lstr(resource='gameCenterText') - elif account_type == 'Game Circle': - account_type_name = ba.Lstr(resource='gameCircleText') else: raise ValueError("unknown account type: '" + str(account_type) + "'") @@ -603,14 +603,15 @@ class AccountSettingsWindow(ba.Window): color=(0.55, 0.5, 0.6), textcolor=(0.75, 0.7, 0.8), autoselect=True, - on_activate_call=_ba.show_online_score_ui, + on_activate_call=ba.internal.show_online_score_ui, size=(button_width, 50), label=account_type_name) if first_selectable is None: first_selectable = btn if ba.app.ui.use_toolbars: ba.widget(edit=btn, - right_widget=_ba.get_special_widget('party_button')) + right_widget=ba.internal.get_special_widget( + 'party_button')) ba.widget(edit=btn, left_widget=bbtn) v -= game_service_button_space * 0.15 else: @@ -652,7 +653,8 @@ class AccountSettingsWindow(ba.Window): first_selectable = btn if ba.app.ui.use_toolbars: ba.widget(edit=btn, - right_widget=_ba.get_special_widget('party_button')) + right_widget=ba.internal.get_special_widget( + 'party_button')) ba.widget(edit=btn, left_widget=bbtn) v -= achievements_button_space * 0.15 else: @@ -680,7 +682,8 @@ class AccountSettingsWindow(ba.Window): first_selectable = btn if ba.app.ui.use_toolbars: ba.widget(edit=btn, - right_widget=_ba.get_special_widget('party_button')) + right_widget=ba.internal.get_special_widget( + 'party_button')) ba.widget(edit=btn, left_widget=bbtn) v -= leaderboards_button_space * 0.15 else: @@ -750,7 +753,8 @@ class AccountSettingsWindow(ba.Window): first_selectable = btn if ba.app.ui.use_toolbars: ba.widget(edit=btn, - right_widget=_ba.get_special_widget('party_button')) + right_widget=ba.internal.get_special_widget( + 'party_button')) ba.widget(edit=btn, left_widget=bbtn) self._linked_accounts_text: ba.Widget | None @@ -805,7 +809,8 @@ class AccountSettingsWindow(ba.Window): first_selectable = btn if ba.app.ui.use_toolbars: ba.widget(edit=btn, - right_widget=_ba.get_special_widget('party_button')) + right_widget=ba.internal.get_special_widget( + 'party_button')) ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50) self._unlink_accounts_button: ba.Widget | None @@ -833,7 +838,8 @@ class AccountSettingsWindow(ba.Window): first_selectable = btn if ba.app.ui.use_toolbars: ba.widget(edit=btn, - right_widget=_ba.get_special_widget('party_button')) + right_widget=ba.internal.get_special_widget( + 'party_button')) ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50) self._update_unlink_accounts_button() else: @@ -854,7 +860,8 @@ class AccountSettingsWindow(ba.Window): first_selectable = btn if ba.app.ui.use_toolbars: ba.widget(edit=btn, - right_widget=_ba.get_special_widget('party_button')) + right_widget=ba.internal.get_special_widget( + 'party_button')) ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15) if show_cancel_v2_sign_in_button: @@ -872,7 +879,8 @@ class AccountSettingsWindow(ba.Window): first_selectable = btn if ba.app.ui.use_toolbars: ba.widget(edit=btn, - right_widget=_ba.get_special_widget('party_button')) + right_widget=ba.internal.get_special_widget( + 'party_button')) ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15) # Whatever the topmost selectable thing is, we want it to scroll all @@ -889,13 +897,13 @@ class AccountSettingsWindow(ba.Window): def _on_achievements_press(self) -> None: # pylint: disable=cyclic-import from bastd.ui import achievements - account_state = _ba.get_v1_account_state() - account_type = (_ba.get_v1_account_type() + account_state = ba.internal.get_v1_account_state() + account_type = (ba.internal.get_v1_account_type() if account_state == 'signed_in' else 'unknown') # for google play we use the built-in UI; otherwise pop up our own if account_type == 'Google Play': ba.timer(0.15, - ba.Call(_ba.show_online_score_ui, 'achievements'), + ba.Call(ba.internal.show_online_score_ui, 'achievements'), timetype=ba.TimeType.REAL) elif account_type != 'unknown': assert self._achievements_button is not None @@ -907,15 +915,16 @@ class AccountSettingsWindow(ba.Window): def _on_leaderboards_press(self) -> None: ba.timer(0.15, - ba.Call(_ba.show_online_score_ui, 'leaderboards'), + ba.Call(ba.internal.show_online_score_ui, 'leaderboards'), timetype=ba.TimeType.REAL) def _have_unlinkable_accounts(self) -> bool: # if this is not present, we haven't had contact from the server so # let's not proceed.. - if _ba.get_public_login_id() is None: + if ba.internal.get_public_login_id() is None: return False - accounts = _ba.get_v1_account_misc_read_val_2('linkedAccounts', []) + accounts = ba.internal.get_v1_account_misc_read_val_2( + 'linkedAccounts', []) return len(accounts) > 1 def _update_unlink_accounts_button(self) -> None: @@ -933,11 +942,12 @@ class AccountSettingsWindow(ba.Window): # if this is not present, we haven't had contact from the server so # let's not proceed.. - if _ba.get_public_login_id() is None: + if ba.internal.get_public_login_id() is None: num = int(time.time()) % 4 accounts_str = num * '.' + (4 - num) * ' ' else: - accounts = _ba.get_v1_account_misc_read_val_2('linkedAccounts', []) + accounts = ba.internal.get_v1_account_misc_read_val_2( + 'linkedAccounts', []) # our_account = _bs.get_v1_account_display_string() # accounts = [a for a in accounts if a != our_account] # accounts_str = u', '.join(accounts) if accounts else @@ -977,7 +987,7 @@ class AccountSettingsWindow(ba.Window): if self._tickets_text is None: return try: - tc_str = str(_ba.get_v1_account_ticket_count()) + tc_str = str(ba.internal.get_v1_account_ticket_count()) except Exception: ba.print_exception() tc_str = '-' @@ -989,7 +999,7 @@ class AccountSettingsWindow(ba.Window): if self._account_name_text is None: return try: - name_str = _ba.get_v1_account_display_string() + name_str = ba.internal.get_v1_account_display_string() except Exception: ba.print_exception() name_str = '??' @@ -1043,7 +1053,7 @@ class AccountSettingsWindow(ba.Window): if ba.app.accounts_v2.have_primary_credentials(): ba.app.accounts_v2.set_primary_credentials(None) else: - _ba.sign_out_v1() + ba.internal.sign_out_v1() cfg = ba.app.config @@ -1061,7 +1071,7 @@ class AccountSettingsWindow(ba.Window): account_type: str, show_test_warning: bool = True) -> None: del show_test_warning # unused - _ba.sign_in_v1(account_type) + ba.internal.sign_in_v1(account_type) # Make note of the type account we're *wanting* to be signed in with. cfg = ba.app.config @@ -1082,7 +1092,7 @@ class AccountSettingsWindow(ba.Window): # FIXME: This would need to happen server-side these days. if self._can_reset_achievements: ba.app.config['Achievements'] = {} - _ba.reset_achievements() + ba.internal.reset_achievements() campaign = getcampaign('Default') campaign.reset() # also writes the config.. campaign = getcampaign('Challenges') diff --git a/dist/ba_data/python/bastd/ui/account/unlink.py b/dist/ba_data/python/bastd/ui/account/unlink.py index 6cff2c4..84dffb9 100644 --- a/dist/ba_data/python/bastd/ui/account/unlink.py +++ b/dist/ba_data/python/bastd/ui/account/unlink.py @@ -7,8 +7,8 @@ from __future__ import annotations import time from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: from typing import Any @@ -77,11 +77,11 @@ class AccountUnlinkWindow(ba.Window): margin=0, left_border=10) - our_login_id = _ba.get_public_login_id() + our_login_id = ba.internal.get_public_login_id() if our_login_id is None: entries = [] else: - account_infos = _ba.get_v1_account_misc_read_val_2( + account_infos = ba.internal.get_v1_account_misc_read_val_2( 'linkedAccounts2', []) entries = [{ 'name': ai['d'], @@ -108,12 +108,12 @@ class AccountUnlinkWindow(ba.Window): ba.screenmessage(ba.Lstr(resource='pleaseWaitText', fallback_resource='requestingText'), color=(0, 1, 0)) - _ba.add_transaction({ + ba.internal.add_transaction({ 'type': 'ACCOUNT_UNLINK_REQUEST', 'accountID': entry['id'], 'expire_time': time.time() + 5 }) - _ba.run_transactions() + ba.internal.run_transactions() ba.containerwidget(edit=self._root_widget, transition=self._transition_out) diff --git a/dist/ba_data/python/bastd/ui/account/v2.py b/dist/ba_data/python/bastd/ui/account/v2.py index 1f9fea3..38047c2 100644 --- a/dist/ba_data/python/bastd/ui/account/v2.py +++ b/dist/ba_data/python/bastd/ui/account/v2.py @@ -8,7 +8,7 @@ import logging from typing import TYPE_CHECKING import ba -import _ba +import ba.internal from efro.error import CommunicationError import bacommon.cloud @@ -81,7 +81,8 @@ class V2SignInWindow(ba.Window): return # Show link(s) the user can use to log in. - address = _ba.get_master_server_address(version=2) + response.url + address = ba.internal.get_master_server_address( + version=2) + response.url address_pretty = address.removeprefix('https://') ba.textwidget( @@ -123,7 +124,7 @@ class V2SignInWindow(ba.Window): position=(self._width * 0.5 - qr_size * 0.5, self._height * 0.36 + qroffs - qr_size * 0.5), size=(qr_size, qr_size), - texture=_ba.get_qrcode_texture(address)) + texture=ba.internal.get_qrcode_texture(address)) # Start querying for results. self._proxyid = response.proxyid diff --git a/dist/ba_data/python/bastd/ui/account/viewer.py b/dist/ba_data/python/bastd/ui/account/viewer.py index da5fb74..d62848f 100644 --- a/dist/ba_data/python/bastd/ui/account/viewer.py +++ b/dist/ba_data/python/bastd/ui/account/viewer.py @@ -6,8 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba +import ba.internal from bastd.ui import popup if TYPE_CHECKING: @@ -91,8 +91,9 @@ class AccountViewerWindow(popup.PopupWindow): # In cases where the user most likely has a browser/email, lets # offer a 'report this user' button. - if (is_browser_likely_available() and _ba.get_v1_account_misc_read_val( - 'showAccountExtrasMenu', False)): + if (is_browser_likely_available() + and ba.internal.get_v1_account_misc_read_val( + 'showAccountExtrasMenu', False)): self._extras_menu_button = ba.buttonwidget( parent=self.root_widget, @@ -154,11 +155,11 @@ class AccountViewerWindow(popup.PopupWindow): delegate=self) def _on_ban_press(self) -> None: - _ba.add_transaction({ + ba.internal.add_transaction({ 'type': 'BAN_ACCOUNT', 'account': self._account_id }) - _ba.run_transactions() + ba.internal.run_transactions() def _on_report_press(self) -> None: from bastd.ui import report @@ -166,8 +167,8 @@ class AccountViewerWindow(popup.PopupWindow): origin_widget=self._extras_menu_button) def _on_more_press(self) -> None: - ba.open_url(_ba.get_master_server_address() + '/highscores?profile=' + - self._account_id) + ba.open_url(ba.internal.get_master_server_address() + + '/highscores?profile=' + self._account_id) def _on_query_response(self, data: dict[str, Any] | None) -> None: # FIXME: Tidy this up. @@ -197,8 +198,8 @@ class AccountViewerWindow(popup.PopupWindow): ba.print_exception('Error displaying trophies.') account_name_spacing = 15 tscale = 0.65 - ts_height = _ba.get_string_height(trophystr, - suppress_warning=True) + ts_height = ba.internal.get_string_height( + trophystr, suppress_warning=True) sub_width = self._width - 80 sub_height = 200 + ts_height * tscale + \ account_name_spacing * len(data['accountDisplayStrings']) @@ -321,8 +322,8 @@ class AccountViewerWindow(popup.PopupWindow): ('${SUFFIX}', '')]).evaluate() rank_str_width = min( sub_width * maxwidth_scale, - _ba.get_string_width(rank_str, suppress_warning=True) * - 0.55) + ba.internal.get_string_width( + rank_str, suppress_warning=True) * 0.55) # Only tack our suffix on if its at the end and only for # non-diamond leagues. @@ -374,8 +375,8 @@ class AccountViewerWindow(popup.PopupWindow): ]).evaluate() rank_str_width = min( sub_width * maxwidth_scale, - _ba.get_string_width(rank_str, suppress_warning=True) * - 0.3) + ba.internal.get_string_width( + rank_str, suppress_warning=True) * 0.3) # Only tack our suffix on if its at the end and only for # non-diamond leagues. diff --git a/dist/ba_data/python/bastd/ui/appinvite.py b/dist/ba_data/python/bastd/ui/appinvite.py index 98ba818..176be36 100644 --- a/dist/ba_data/python/bastd/ui/appinvite.py +++ b/dist/ba_data/python/bastd/ui/appinvite.py @@ -8,8 +8,8 @@ import copy import time from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: from typing import Any @@ -62,11 +62,11 @@ class AppInviteWindow(ba.Window): 'gatherWindow.earnTicketsForRecommendingText'), subs=[('${COUNT}', str( - _ba.get_v1_account_misc_read_val( + ba.internal.get_v1_account_misc_read_val( 'friendTryTickets', 300))), ('${YOU_COUNT}', str( - _ba.get_v1_account_misc_read_val( + ba.internal.get_v1_account_misc_read_val( 'friendTryAwardTickets', 100)))])) or_text = ba.Lstr(resource='orText', @@ -104,14 +104,14 @@ class AppInviteWindow(ba.Window): on_activate_call=ba.WeakCall(self._send_code)) # kick off a transaction to get our code - _ba.add_transaction( + ba.internal.add_transaction( { 'type': 'FRIEND_PROMO_CODE_REQUEST', 'ali': False, 'expire_time': time.time() + 20 }, callback=ba.WeakCall(self._on_code_result)) - _ba.run_transactions() + ba.internal.run_transactions() def _on_code_result(self, result: dict[str, Any] | None) -> None: if result is not None: @@ -128,18 +128,18 @@ class AppInviteWindow(ba.Window): ba.playsound(ba.getsound('error')) return - if _ba.get_v1_account_state() == 'signed_in': + if ba.internal.get_v1_account_state() == 'signed_in': ba.set_analytics_screen('App Invite UI') - _ba.show_app_invite( + ba.internal.show_app_invite( ba.Lstr(resource='gatherWindow.appInviteTitleText', subs=[('${APP_NAME}', ba.Lstr(resource='titleText')) ]).evaluate(), ba.Lstr(resource='gatherWindow.appInviteMessageText', - subs=[ - ('${COUNT}', str(self._data['tickets'])), - ('${NAME}', _ba.get_v1_account_name().split()[0]), - ('${APP_NAME}', ba.Lstr(resource='titleText')) - ]).evaluate(), self._data['code']) + subs=[('${COUNT}', str(self._data['tickets'])), + ('${NAME}', + ba.internal.get_v1_account_name().split()[0]), + ('${APP_NAME}', ba.Lstr(resource='titleText')) + ]).evaluate(), self._data['code']) else: ba.playsound(ba.getsound('error')) @@ -250,13 +250,14 @@ class ShowFriendCodeWindow(ba.Window): def _google_invites(self) -> None: ba.set_analytics_screen('App Invite UI') - _ba.show_app_invite( + ba.internal.show_app_invite( ba.Lstr(resource='gatherWindow.appInviteTitleText', subs=[('${APP_NAME}', ba.Lstr(resource='titleText')) ]).evaluate(), ba.Lstr(resource='gatherWindow.appInviteMessageText', subs=[('${COUNT}', str(self._data['tickets'])), - ('${NAME}', _ba.get_v1_account_name().split()[0]), + ('${NAME}', + ba.internal.get_v1_account_name().split()[0]), ('${APP_NAME}', ba.Lstr(resource='titleText')) ]).evaluate(), self._data['code']) @@ -264,7 +265,7 @@ class ShowFriendCodeWindow(ba.Window): import urllib.parse # If somehow we got signed out. - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': ba.screenmessage(ba.Lstr(resource='notSignedInText'), color=(1, 0, 0)) ba.playsound(ba.getsound('error')) @@ -273,7 +274,7 @@ class ShowFriendCodeWindow(ba.Window): ba.set_analytics_screen('Email Friend Code') subject = (ba.Lstr(resource='gatherWindow.friendHasSentPromoCodeText'). evaluate().replace( - '${NAME}', _ba.get_v1_account_name()).replace( + '${NAME}', ba.internal.get_v1_account_name()).replace( '${APP_NAME}', ba.Lstr(resource='titleText').evaluate()).replace( '${COUNT}', str(self._data['tickets']))) @@ -304,7 +305,7 @@ def handle_app_invites_press(force_code: bool = False) -> None: """(internal)""" app = ba.app do_app_invites = (app.platform == 'android' and app.subplatform == 'google' - and _ba.get_v1_account_misc_read_val( + and ba.internal.get_v1_account_misc_read_val( 'enableAppInvites', False) and not app.on_tv) if force_code: do_app_invites = False @@ -326,11 +327,11 @@ def handle_app_invites_press(force_code: bool = False) -> None: else: ShowFriendCodeWindow(result) - _ba.add_transaction( + ba.internal.add_transaction( { 'type': 'FRIEND_PROMO_CODE_REQUEST', 'ali': False, 'expire_time': time.time() + 10 }, callback=handle_result) - _ba.run_transactions() + ba.internal.run_transactions() diff --git a/dist/ba_data/python/bastd/ui/characterpicker.py b/dist/ba_data/python/bastd/ui/characterpicker.py index 353ee38..62f78bc 100644 --- a/dist/ba_data/python/bastd/ui/characterpicker.py +++ b/dist/ba_data/python/bastd/ui/characterpicker.py @@ -7,8 +7,8 @@ from __future__ import annotations import math from typing import TYPE_CHECKING -import _ba import ba +import ba.internal from bastd.ui import popup if TYPE_CHECKING: @@ -156,7 +156,7 @@ class CharacterPicker(popup.PopupWindow): def _on_store_press(self) -> None: from bastd.ui.account import show_sign_in_prompt from bastd.ui.store.browser import StoreBrowserWindow - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return self._transition_out() diff --git a/dist/ba_data/python/bastd/ui/configerror.py b/dist/ba_data/python/bastd/ui/configerror.py index b94c580..26ffdf6 100644 --- a/dist/ba_data/python/bastd/ui/configerror.py +++ b/dist/ba_data/python/bastd/ui/configerror.py @@ -6,8 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: pass @@ -29,7 +29,7 @@ class ConfigErrorWindow(ba.Window): h_align='center', v_align='top', scale=0.73, - text=(f'Error reading {_ba.appnameupper()} config file' + text=(f'Error reading {ba.internal.appnameupper()} config file' ':\n\n\nCheck the console' ' (press ~ twice) for details.\n\nWould you like to quit and' ' try to fix it by hand\nor overwrite it with defaults?\n\n' @@ -58,10 +58,10 @@ class ConfigErrorWindow(ba.Window): def _quit(self) -> None: ba.timer(0.001, self._edit_and_quit, timetype=ba.TimeType.REAL) - _ba.lock_all_input() + ba.internal.lock_all_input() def _edit_and_quit(self) -> None: - _ba.open_file_externally(self._config_file_path) + ba.internal.open_file_externally(self._config_file_path) ba.timer(0.1, ba.quit, timetype=ba.TimeType.REAL) def _defaults(self) -> None: diff --git a/dist/ba_data/python/bastd/ui/confirm.py b/dist/ba_data/python/bastd/ui/confirm.py index 11c76d9..46bb90b 100644 --- a/dist/ba_data/python/bastd/ui/confirm.py +++ b/dist/ba_data/python/bastd/ui/confirm.py @@ -6,8 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: from typing import Any, Callable @@ -54,7 +54,7 @@ class ConfirmWindow: size=(width, height), transition=transition, toolbar_visibility='menu_minimal_no_back', - parent=_ba.get_special_widget('overlay_stack'), + parent=ba.internal.get_special_widget('overlay_stack'), scale=(2.1 if uiscale is ba.UIScale.SMALL else 1.5 if uiscale is ba.UIScale.MEDIUM else 1.0), scale_origin_stack_offset=scale_origin) @@ -147,12 +147,13 @@ class QuitWindow: origin_widget=origin_widget).root_widget) def _fade_and_quit(self) -> None: - _ba.fade_screen(False, - time=0.2, - endcall=lambda: ba.quit(soft=True, back=self._back)) - _ba.lock_all_input() + ba.internal.fade_screen( + False, + time=0.2, + endcall=lambda: ba.quit(soft=True, back=self._back)) + ba.internal.lock_all_input() # Unlock and fade back in shortly.. just in case something goes wrong # (or on android where quit just backs out of our activity and # we may come back) - ba.timer(0.3, _ba.unlock_all_input, timetype=ba.TimeType.REAL) + ba.timer(0.3, ba.internal.unlock_all_input, timetype=ba.TimeType.REAL) diff --git a/dist/ba_data/python/bastd/ui/continues.py b/dist/ba_data/python/bastd/ui/continues.py index 861c860..85e9b7b 100644 --- a/dist/ba_data/python/bastd/ui/continues.py +++ b/dist/ba_data/python/bastd/ui/continues.py @@ -7,8 +7,8 @@ from __future__ import annotations import weakref from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: from typing import Any, Callable @@ -37,11 +37,14 @@ class ContinuesWindow(ba.Window): txt = (ba.Lstr( resource='continuePurchaseText').evaluate().split('${PRICE}')) t_left = txt[0] - t_left_width = _ba.get_string_width(t_left, suppress_warning=True) + t_left_width = ba.internal.get_string_width(t_left, + suppress_warning=True) t_price = ba.charstr(ba.SpecialChar.TICKET) + str(self._cost) - t_price_width = _ba.get_string_width(t_price, suppress_warning=True) + t_price_width = ba.internal.get_string_width(t_price, + suppress_warning=True) t_right = txt[-1] - t_right_width = _ba.get_string_width(t_right, suppress_warning=True) + t_right_width = ba.internal.get_string_width(t_right, + suppress_warning=True) width_total_half = (t_left_width + t_price_width + t_right_width) * 0.5 ba.textwidget(parent=self._root_widget, @@ -133,8 +136,15 @@ class ContinuesWindow(ba.Window): ba.WeakCall(self._tick), repeat=True, timetype=ba.TimeType.REAL) + + # If there is foreground activity, suspend it. + ba.app.pause() self._tick() + def __del__(self) -> None: + # If there is suspended foreground activity, resume it. + ba.app.resume() + def _tick(self) -> None: # if our target activity is gone or has ended, go away activity = self._activity() @@ -142,9 +152,9 @@ class ContinuesWindow(ba.Window): self._on_cancel() return - if _ba.get_v1_account_state() == 'signed_in': + if ba.internal.get_v1_account_state() == 'signed_in': sval = (ba.charstr(ba.SpecialChar.TICKET) + - str(_ba.get_v1_account_ticket_count())) + str(ba.internal.get_v1_account_ticket_count())) else: sval = '?' if self._tickets_text is not None: @@ -176,14 +186,14 @@ class ContinuesWindow(ba.Window): ba.playsound(ba.getsound('error')) else: # If somehow we got signed out... - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': ba.screenmessage(ba.Lstr(resource='notSignedInText'), color=(1, 0, 0)) ba.playsound(ba.getsound('error')) return # If it appears we don't have enough tickets, offer to buy more. - tickets = _ba.get_v1_account_ticket_count() + tickets = ba.internal.get_v1_account_ticket_count() if tickets < self._cost: # FIXME: Should we start the timer back up again after? self._counting_down = False diff --git a/dist/ba_data/python/bastd/ui/coop/browser.py b/dist/ba_data/python/bastd/ui/coop/browser.py index 84d0ccf..b6d978d 100644 --- a/dist/ba_data/python/bastd/ui/coop/browser.py +++ b/dist/ba_data/python/bastd/ui/coop/browser.py @@ -8,8 +8,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba +import ba.internal from bastd.ui.store.button import StoreButton from bastd.ui.league.rankbutton import LeagueRankButton from bastd.ui.store.browser import StoreBrowserWindow @@ -26,7 +26,7 @@ class CoopBrowserWindow(ba.Window): def _update_corner_button_positions(self) -> None: uiscale = ba.app.ui.uiscale offs = (-55 if uiscale is ba.UIScale.SMALL - and _ba.is_party_icon_visible() else 0) + and ba.internal.is_party_icon_visible() else 0) if self._league_rank_button is not None: self._league_rank_button.set_position( (self._width - 282 + offs - self._x_inset, self._height - 85 - @@ -54,7 +54,7 @@ class CoopBrowserWindow(ba.Window): # Quick note to players that tourneys won't work in ballistica # core builds. (need to split the word so it won't get subbed out) - if 'ballistica' + 'core' == _ba.appname(): + if 'ballistica' + 'core' == ba.internal.appname(): ba.timer(1.0, lambda: ba.screenmessage( ba.Lstr(resource='noTournamentsInTestBuildText'), @@ -93,7 +93,7 @@ class CoopBrowserWindow(ba.Window): self._tourney_data_up_to_date = False - self._campaign_difficulty = _ba.get_v1_account_misc_val( + self._campaign_difficulty = ba.internal.get_v1_account_misc_val( 'campaignDifficulty', 'easy') super().__init__(root_widget=ba.containerwidget( @@ -234,7 +234,7 @@ class CoopBrowserWindow(ba.Window): self._subcontainer: ba.Widget | None = None # Take note of our account state; we'll refresh later if this changes. - self._account_state_num = _ba.get_v1_account_state_num() + self._account_state_num = ba.internal.get_v1_account_state_num() # Same for fg/bg state. self._fg_state = app.fg_state @@ -252,7 +252,7 @@ class CoopBrowserWindow(ba.Window): # starting point. if (app.accounts_v1.account_tournament_list is not None and app.accounts_v1.account_tournament_list[0] - == _ba.get_v1_account_state_num() and all( + == ba.internal.get_v1_account_state_num() and all( t_id in app.accounts_v1.tournament_info for t_id in app.accounts_v1.account_tournament_list[1])): tourney_data = [ @@ -300,7 +300,7 @@ class CoopBrowserWindow(ba.Window): self._tourney_data_up_to_date = False # If our account state has changed, do a full request. - account_state_num = _ba.get_v1_account_state_num() + account_state_num = ba.internal.get_v1_account_state_num() if account_state_num != self._account_state_num: self._account_state_num = account_state_num self._save_state() @@ -324,7 +324,7 @@ class CoopBrowserWindow(ba.Window): self._fg_state = ba.app.fg_state self._last_tournament_query_time = cur_time self._doing_tournament_query = True - _ba.tournament_query( + ba.internal.tournament_query( args={ 'source': 'coop window refresh', 'numScores': 1 @@ -333,7 +333,7 @@ class CoopBrowserWindow(ba.Window): ) # Decrement time on our tournament buttons. - ads_enabled = _ba.have_incentivized_ad() + ads_enabled = ba.internal.have_incentivized_ad() for tbtn in self._tournament_buttons: tbtn.time_remaining = max(0, tbtn.time_remaining - 1) if tbtn.time_remaining_value_text is not None: @@ -346,7 +346,7 @@ class CoopBrowserWindow(ba.Window): and self._tourney_data_up_to_date) else '-') # Also adjust the ad icon visibility. - if tbtn.allow_ads and _ba.has_video_ads(): + if tbtn.allow_ads and ba.internal.has_video_ads(): ba.imagewidget(edit=tbtn.entry_fee_ad_image, opacity=1.0 if ads_enabled else 0.25) ba.textwidget(edit=tbtn.entry_fee_text_remaining, @@ -395,11 +395,9 @@ class CoopBrowserWindow(ba.Window): accounts.cache_tournament_info(tournament_data) # Also cache the current tourney list/order for this account. - accounts.account_tournament_list = (_ba.get_v1_account_state_num(), - [ - e['tournamentID'] - for e in tournament_data - ]) + accounts.account_tournament_list = ( + ba.internal.get_v1_account_state_num(), + [e['tournamentID'] for e in tournament_data]) self._doing_tournament_query = False self._update_for_data(tournament_data) @@ -417,7 +415,7 @@ class CoopBrowserWindow(ba.Window): print('ERROR: invalid campaign difficulty:', difficulty) difficulty = 'easy' self._campaign_difficulty = difficulty - _ba.add_transaction({ + ba.internal.add_transaction({ 'type': 'SET_MISC_VAL', 'name': 'campaignDifficulty', 'value': difficulty @@ -638,7 +636,7 @@ class CoopBrowserWindow(ba.Window): # FIXME shouldn't use hard-coded strings here. txt = ba.Lstr(resource='tournamentsText', fallback_resource='tournamentText').evaluate() - t_width = _ba.get_string_width(txt, suppress_warning=True) + t_width = ba.internal.get_string_width(txt, suppress_warning=True) ba.textwidget(parent=w_parent, position=(h_base + 27, v + 30), size=(0, 0), @@ -668,7 +666,7 @@ class CoopBrowserWindow(ba.Window): # no tournaments). if self._tournament_button_count == 0: unavailable_text = ba.Lstr(resource='unavailableText') - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': unavailable_text = ba.Lstr( value='${A} (${B})', subs=[('${A}', unavailable_text), @@ -744,8 +742,9 @@ class CoopBrowserWindow(ba.Window): ] # Show easter-egg-hunt either if its easter or we own it. - if _ba.get_v1_account_misc_read_val( - 'easter', False) or _ba.get_purchased('games.easter_egg_hunt'): + if ba.internal.get_v1_account_misc_read_val( + 'easter', + False) or ba.internal.get_purchased('games.easter_egg_hunt'): items = [ 'Challenges:Easter Egg Hunt', 'Challenges:Pro Easter Egg Hunt', @@ -838,7 +837,7 @@ class CoopBrowserWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui.account import show_sign_in_prompt from bastd.ui.league.rankwindow import LeagueRankWindow - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return self._save_state() @@ -855,7 +854,7 @@ class CoopBrowserWindow(ba.Window): ) -> None: # pylint: disable=cyclic-import from bastd.ui.account import show_sign_in_prompt - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return self._save_state() @@ -893,7 +892,7 @@ class CoopBrowserWindow(ba.Window): if game in ('Challenges:Infinite Runaround', 'Challenges:Infinite Onslaught' ) and not ba.app.accounts_v1.have_pro(): - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': show_sign_in_prompt() else: PurchaseWindow(items=['pro']) @@ -920,8 +919,8 @@ class CoopBrowserWindow(ba.Window): required_purchase = None if (required_purchase is not None - and not _ba.get_purchased(required_purchase)): - if _ba.get_v1_account_state() != 'signed_in': + and not ba.internal.get_purchased(required_purchase)): + if ba.internal.get_v1_account_state() != 'signed_in': show_sign_in_prompt() else: PurchaseWindow(items=[required_purchase]) @@ -937,10 +936,17 @@ class CoopBrowserWindow(ba.Window): from bastd.ui.account import show_sign_in_prompt from bastd.ui.tournamententry import TournamentEntryWindow - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return + if ba.internal.workspaces_in_use(): + ba.screenmessage( + ba.Lstr(resource='tournamentsDisabledWorkspaceText'), + color=(1, 0, 0)) + ba.playsound(ba.getsound('error')) + return + if not self._tourney_data_up_to_date: ba.screenmessage(ba.Lstr(resource='tournamentCheckingStateText'), color=(1, 1, 0)) diff --git a/dist/ba_data/python/bastd/ui/coop/gamebutton.py b/dist/ba_data/python/bastd/ui/coop/gamebutton.py index fb819f3..6324783 100644 --- a/dist/ba_data/python/bastd/ui/coop/gamebutton.py +++ b/dist/ba_data/python/bastd/ui/coop/gamebutton.py @@ -7,7 +7,6 @@ from __future__ import annotations import random from typing import TYPE_CHECKING -import _ba import ba if TYPE_CHECKING: @@ -200,17 +199,17 @@ class GameButton: 'Challenges:Infinite Onslaught') and not ba.app.accounts_v1.have_pro()) or (game in ('Challenges:Meteor Shower', ) - and not _ba.get_purchased('games.meteor_shower')) + and not ba.internal.get_purchased('games.meteor_shower')) or (game in ('Challenges:Target Practice', 'Challenges:Target Practice B') - and not _ba.get_purchased('games.target_practice')) + and not ba.internal.get_purchased('games.target_practice')) or (game in ('Challenges:Ninja Fight', ) - and not _ba.get_purchased('games.ninja_fight')) + and not ba.internal.get_purchased('games.ninja_fight')) or (game in ('Challenges:Pro Ninja Fight', ) - and not _ba.get_purchased('games.ninja_fight')) - or (game in ('Challenges:Easter Egg Hunt', - 'Challenges:Pro Easter Egg Hunt') - and not _ba.get_purchased('games.easter_egg_hunt'))): + and not ba.internal.get_purchased('games.ninja_fight')) or + (game in ('Challenges:Easter Egg Hunt', + 'Challenges:Pro Easter Egg Hunt') + and not ba.internal.get_purchased('games.easter_egg_hunt'))): unlocked = False # Let's tint levels a slightly different color when easy mode diff --git a/dist/ba_data/python/bastd/ui/coop/tournamentbutton.py b/dist/ba_data/python/bastd/ui/coop/tournamentbutton.py index f443ef2..041b6c5 100644 --- a/dist/ba_data/python/bastd/ui/coop/tournamentbutton.py +++ b/dist/ba_data/python/bastd/ui/coop/tournamentbutton.py @@ -8,7 +8,7 @@ from typing import TYPE_CHECKING import copy import ba -import _ba +import ba.internal if TYPE_CHECKING: from typing import Any, Callable @@ -499,7 +499,7 @@ class TournamentButton: self.allow_ads = allow_ads = entry['allowAds'] final_fee: int | None = (None if fee_var is None else - _ba.get_v1_account_misc_read_val( + ba.internal.get_v1_account_misc_read_val( fee_var, '?')) final_fee_str: str | ba.Lstr @@ -519,8 +519,8 @@ class TournamentButton: # Now, if this fee allows ads and we support video ads, show # the 'or ad' version. - if allow_ads and _ba.has_video_ads(): - ads_enabled = _ba.have_incentivized_ad() + if allow_ads and ba.internal.has_video_ads(): + ads_enabled = ba.internal.have_incentivized_ad() ba.imagewidget(edit=self.entry_fee_ad_image, opacity=1.0 if ads_enabled else 0.25) or_text = ba.Lstr(resource='orText', diff --git a/dist/ba_data/python/bastd/ui/creditslist.py b/dist/ba_data/python/bastd/ui/creditslist.py index c266fc5..7400005 100644 --- a/dist/ba_data/python/bastd/ui/creditslist.py +++ b/dist/ba_data/python/bastd/ui/creditslist.py @@ -6,8 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: from typing import Sequence @@ -91,17 +91,19 @@ class CreditsListWindow(ba.Window): capture_arrows=True) if ba.app.ui.use_toolbars: - ba.widget(edit=scroll, - right_widget=_ba.get_special_widget('party_button')) + ba.widget( + edit=scroll, + right_widget=ba.internal.get_special_widget('party_button')) if uiscale is ba.UIScale.SMALL: - ba.widget(edit=scroll, - left_widget=_ba.get_special_widget('back_button')) + ba.widget( + edit=scroll, + left_widget=ba.internal.get_special_widget('back_button')) def _format_names(names2: Sequence[str], inset: float) -> str: sval = '' # measure a series since there's overlaps and stuff.. - space_width = _ba.get_string_width(' ' * 10, - suppress_warning=True) / 10.0 + space_width = ba.internal.get_string_width( + ' ' * 10, suppress_warning=True) / 10.0 spacing = 330.0 col1 = inset col2 = col1 + spacing @@ -124,7 +126,8 @@ class CreditsListWindow(ba.Window): spacingstr = ' ' * int((target - line_width) / space_width) nline += spacingstr nline += name - line_width = _ba.get_string_width(nline, suppress_warning=True) + line_width = ba.internal.get_string_width( + nline, suppress_warning=True) if nline != '': sval += nline + '\n' return sval @@ -236,7 +239,7 @@ class CreditsListWindow(ba.Window): '${NAME}', 'the Khronos Group') + '\n' '\n' ' ' - ' www.froemling.net\n') + ' www.ballistica.net\n') txt = credits_text lines = txt.splitlines() diff --git a/dist/ba_data/python/bastd/ui/debug.py b/dist/ba_data/python/bastd/ui/debug.py index 63f3878..e896454 100644 --- a/dist/ba_data/python/bastd/ui/debug.py +++ b/dist/ba_data/python/bastd/ui/debug.py @@ -15,11 +15,12 @@ if TYPE_CHECKING: class DebugWindow(ba.Window): """Window for debugging internal values.""" - def __init__(self, transition: str = 'in_right'): + def __init__(self, transition: str | None = 'in_right'): # pylint: disable=too-many-statements # pylint: disable=cyclic-import from bastd.ui import popup + ba.app.ui.set_main_menu_location('Benchmarks & Stress Tests') uiscale = ba.app.ui.uiscale self._width = width = 580 self._height = height = (350 if uiscale is ba.UIScale.SMALL else diff --git a/dist/ba_data/python/bastd/ui/feedback.py b/dist/ba_data/python/bastd/ui/feedback.py index 6999cca..c4b4665 100644 --- a/dist/ba_data/python/bastd/ui/feedback.py +++ b/dist/ba_data/python/bastd/ui/feedback.py @@ -7,6 +7,7 @@ from __future__ import annotations from typing import TYPE_CHECKING import ba +import ba.internal if TYPE_CHECKING: pass @@ -54,13 +55,12 @@ def ask_for_rating() -> ba.Widget | None: v_align='center') def do_rating() -> None: - import _ba if platform == 'android': - appname = _ba.appname() + appname = ba.internal.appname() if subplatform == 'google': url = f'market://details?id=net.froemling.{appname}' else: - url = 'market://details?id=net.froemling.{appname}cb' + url = f'market://details?id=net.froemling.{appname}cb' else: url = 'macappstore://itunes.apple.com/app/id416482767?ls=1&mt=12' diff --git a/dist/ba_data/python/bastd/ui/fileselector.py b/dist/ba_data/python/bastd/ui/fileselector.py index bb6dc65..1f0963e 100644 --- a/dist/ba_data/python/bastd/ui/fileselector.py +++ b/dist/ba_data/python/bastd/ui/fileselector.py @@ -9,8 +9,8 @@ import threading import time from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: from typing import Any, Callable, Sequence @@ -242,7 +242,7 @@ class FileSelectorWindow(ba.Window): max_str_width = 300.0 str_width = min( max_str_width, - _ba.get_string_width(folder_name, suppress_warning=True)) + ba.internal.get_string_width(folder_name, suppress_warning=True)) ba.textwidget(edit=self._path_text, text=folder_name, maxwidth=max_str_width) diff --git a/dist/ba_data/python/bastd/ui/gather/__init__.py b/dist/ba_data/python/bastd/ui/gather/__init__.py index f3a1eb9..7051393 100644 --- a/dist/ba_data/python/bastd/ui/gather/__init__.py +++ b/dist/ba_data/python/bastd/ui/gather/__init__.py @@ -8,8 +8,8 @@ import weakref from enum import Enum from typing import TYPE_CHECKING -import _ba import ba +import ba.internal from bastd.ui.tabs import TabRow if TYPE_CHECKING: @@ -88,7 +88,7 @@ class GatherWindow(ba.Window): self._transition_out = 'out_right' scale_origin = None ba.app.ui.set_main_menu_location('Gather') - _ba.set_party_icon_always_visible(True) + ba.internal.set_party_icon_always_visible(True) uiscale = ba.app.ui.uiscale self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040 x_offs = 100 if uiscale is ba.UIScale.SMALL else 0 @@ -151,7 +151,8 @@ class GatherWindow(ba.Window): tabdefs: list[tuple[GatherWindow.TabID, ba.Lstr]] = [ (self.TabID.ABOUT, ba.Lstr(resource=self._r + '.aboutText')) ] - if _ba.get_v1_account_misc_read_val('enablePublicParties', True): + if ba.internal.get_v1_account_misc_read_val('enablePublicParties', + True): tabdefs.append((self.TabID.INTERNET, ba.Lstr(resource=self._r + '.publicText'))) tabdefs.append( @@ -186,11 +187,13 @@ class GatherWindow(ba.Window): self._tabs[tab_id] = tabtype(self) if ba.app.ui.use_toolbars: - ba.widget(edit=self._tab_row.tabs[tabdefs[-1][0]].button, - right_widget=_ba.get_special_widget('party_button')) + ba.widget( + edit=self._tab_row.tabs[tabdefs[-1][0]].button, + right_widget=ba.internal.get_special_widget('party_button')) if uiscale is ba.UIScale.SMALL: - ba.widget(edit=self._tab_row.tabs[tabdefs[0][0]].button, - left_widget=_ba.get_special_widget('back_button')) + ba.widget( + edit=self._tab_row.tabs[tabdefs[0][0]].button, + left_widget=ba.internal.get_special_widget('back_button')) self._scroll_width = self._width - scroll_buffer_h self._scroll_height = self._height - 180.0 + tabs_top_extra @@ -214,7 +217,7 @@ class GatherWindow(ba.Window): self._restore_state() def __del__(self) -> None: - _ba.set_party_icon_always_visible(False) + ba.internal.set_party_icon_always_visible(False) def playlist_select(self, origin_widget: ba.Widget) -> None: """Called by the private-hosting tab to select a playlist.""" diff --git a/dist/ba_data/python/bastd/ui/gather/abouttab.py b/dist/ba_data/python/bastd/ui/gather/abouttab.py index a782e6c..23dfcce 100644 --- a/dist/ba_data/python/bastd/ui/gather/abouttab.py +++ b/dist/ba_data/python/bastd/ui/gather/abouttab.py @@ -7,7 +7,7 @@ from __future__ import annotations from typing import TYPE_CHECKING import ba -import _ba +import ba.internal from bastd.ui.gather import GatherTab if TYPE_CHECKING: @@ -51,8 +51,8 @@ class AboutGatherTab(GatherTab): include_invite = True msc_scale = 1.1 c_height_2 = min(region_height, string_height * msc_scale + 100) - try_tickets = _ba.get_v1_account_misc_read_val('friendTryTickets', - None) + try_tickets = ba.internal.get_v1_account_misc_read_val( + 'friendTryTickets', None) if try_tickets is None: include_invite = False self._container = ba.containerwidget( @@ -106,7 +106,7 @@ class AboutGatherTab(GatherTab): def _invite_to_try_press(self) -> None: from bastd.ui.account import show_sign_in_prompt from bastd.ui.appinvite import handle_app_invites_press - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return handle_app_invites_press() diff --git a/dist/ba_data/python/bastd/ui/gather/manualtab.py b/dist/ba_data/python/bastd/ui/gather/manualtab.py index 0ca6150..0fa1a7b 100644 --- a/dist/ba_data/python/bastd/ui/gather/manualtab.py +++ b/dist/ba_data/python/bastd/ui/gather/manualtab.py @@ -11,8 +11,8 @@ from enum import Enum from dataclasses import dataclass from bastd.ui.gather import GatherTab -import _ba import ba +import ba.internal if TYPE_CHECKING: from typing import Any, Callable @@ -341,8 +341,9 @@ class ManualGatherTab(GatherTab): label=ba.Lstr(resource='gatherWindow.manualConnectText'), autoselect=True) if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: - ba.widget(edit=btn1, - left_widget=_ba.get_special_widget('back_button')) + ba.widget( + edit=btn1, + left_widget=ba.internal.get_special_widget('back_button')) btnv -= b_height + b_space_extra ba.buttonwidget(parent=self._container, size=(b_width, b_height), @@ -686,7 +687,7 @@ class ManualGatherTab(GatherTab): config = ba.app.config config['Last Manual Party Connect Address'] = resolved_address config.commit() - _ba.connect_to_party(resolved_address, port=port) + ba.internal.connect_to_party(resolved_address, port=port) def _run_addr_fetch(self) -> None: try: @@ -894,9 +895,12 @@ class ManualGatherTab(GatherTab): if t_accessible_extra: ba.textwidget( edit=t_accessible_extra, - text=ba.Lstr(resource='gatherWindow.' - 'manualRouterForwardingText', - subs=[('${PORT}', - str(_ba.get_game_port()))]), + text=ba.Lstr( + resource='gatherWindow.' + 'manualRouterForwardingText', + subs=[ + ('${PORT}', str(ba.internal.get_game_port())), + ], + ), color=color_bad, ) diff --git a/dist/ba_data/python/bastd/ui/gather/nearbytab.py b/dist/ba_data/python/bastd/ui/gather/nearbytab.py index 4622d5a..c5ebcdd 100644 --- a/dist/ba_data/python/bastd/ui/gather/nearbytab.py +++ b/dist/ba_data/python/bastd/ui/gather/nearbytab.py @@ -8,7 +8,7 @@ import weakref from typing import TYPE_CHECKING import ba -import _ba +import ba.internal from bastd.ui.gather import GatherTab if TYPE_CHECKING: @@ -42,13 +42,13 @@ class NetScanner: ba.timer(0.25, ba.WeakCall(self.update), timetype=ba.TimeType.REAL) def __del__(self) -> None: - _ba.end_host_scanning() + ba.internal.end_host_scanning() def _on_select(self, host: dict[str, Any]) -> None: self._last_selected_host = host def _on_activate(self, host: dict[str, Any]) -> None: - _ba.connect_to_party(host['address']) + ba.internal.connect_to_party(host['address']) def update(self) -> None: """(internal)""" @@ -65,7 +65,7 @@ class NetScanner: # Grab this now this since adding widgets will change it. last_selected_host = self._last_selected_host - hosts = _ba.host_scan_cycle() + hosts = ba.internal.host_scan_cycle() for i, host in enumerate(hosts): txt3 = ba.textwidget(parent=self._columnwidget, size=(self._width / t_scale, 30), diff --git a/dist/ba_data/python/bastd/ui/gather/privatetab.py b/dist/ba_data/python/bastd/ui/gather/privatetab.py index 4eac79e..3f7dcf1 100644 --- a/dist/ba_data/python/bastd/ui/gather/privatetab.py +++ b/dist/ba_data/python/bastd/ui/gather/privatetab.py @@ -11,11 +11,11 @@ from enum import Enum from dataclasses import dataclass from typing import TYPE_CHECKING, cast -import ba -import _ba from efro.dataclassio import dataclass_from_dict, dataclass_to_dict from bacommon.net import (PrivateHostingState, PrivateHostingConfig, PrivatePartyConnectResult) +import ba +import ba.internal from bastd.ui.gather import GatherTab from bastd.ui import getcurrency @@ -184,7 +184,9 @@ class PrivateGatherTab(GatherTab): if playlist is None: playlist = pvars.get_default_list_call() - hcfg.playlist = filter_playlist(playlist, sessiontype) + hcfg.playlist = filter_playlist(playlist, + sessiontype, + name=playlist_name) randomize = cfg.get(f'{pvars.config_name} Playlist Randomize') if not isinstance(randomize, bool): @@ -225,7 +227,7 @@ class PrivateGatherTab(GatherTab): def _update_currency_ui(self) -> None: # Keep currency count up to date if applicable. try: - t_str = str(_ba.get_v1_account_ticket_count()) + t_str = str(ba.internal.get_v1_account_ticket_count()) except Exception: t_str = '?' if self._get_tickets_button: @@ -245,7 +247,7 @@ class PrivateGatherTab(GatherTab): if self._state.sub_tab is SubTabType.HOST: # If we're not signed in, just refresh to show that. - if (_ba.get_v1_account_state() != 'signed_in' + if (ba.internal.get_v1_account_state() != 'signed_in' and self._showing_not_signed_in_screen): self._refresh_sub_tab() else: @@ -254,8 +256,8 @@ class PrivateGatherTab(GatherTab): if (self._last_hosting_state_query_time is None or now - self._last_hosting_state_query_time > 15.0): self._debug_server_comm('querying private party state') - if _ba.get_v1_account_state() == 'signed_in': - _ba.add_transaction( + if ba.internal.get_v1_account_state() == 'signed_in': + ba.internal.add_transaction( { 'type': 'PRIVATE_PARTY_QUERY', 'expire_time': time.time() + 20, @@ -263,7 +265,7 @@ class PrivateGatherTab(GatherTab): callback=ba.WeakCall( self._hosting_state_idle_response), ) - _ba.run_transactions() + ba.internal.run_transactions() else: self._hosting_state_idle_response(None) self._last_hosting_state_query_time = now @@ -436,7 +438,7 @@ class PrivateGatherTab(GatherTab): # pylint: disable=too-many-branches # pylint: disable=too-many-statements - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': ba.textwidget(parent=self._container, size=(0, 0), h_align='center', @@ -702,7 +704,7 @@ class PrivateGatherTab(GatherTab): btnlabel = ba.Lstr( resource='gatherWindow.hostingUnavailableText') elif self._hostingstate.party_code is None: - ticon = _ba.charstr(ba.SpecialChar.TICKET) + ticon = ba.internal.charstr(ba.SpecialChar.TICKET) nowtickets = self._hostingstate.tickets_to_host_now if nowtickets > 0: btnlabel = ba.Lstr( @@ -760,7 +762,7 @@ class PrivateGatherTab(GatherTab): self._connect_press_time = now self._debug_server_comm('sending private party connect') - _ba.add_transaction( + ba.internal.add_transaction( { 'type': 'PRIVATE_PARTY_CONNECT', 'expire_time': time.time() + 20, @@ -768,14 +770,14 @@ class PrivateGatherTab(GatherTab): }, callback=ba.WeakCall(self._connect_response), ) - _ba.run_transactions() + ba.internal.run_transactions() def _start_stop_button_press(self) -> None: if (self._waiting_for_start_stop_response or self._waiting_for_initial_state): return - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': ba.screenmessage(ba.Lstr(resource='notSignedInErrorText')) ba.playsound(ba.getsound('error')) self._refresh_sub_tab() @@ -794,7 +796,7 @@ class PrivateGatherTab(GatherTab): if self._hostingstate.tickets_to_host_now > 0: ticket_count: int | None try: - ticket_count = _ba.get_v1_account_ticket_count() + ticket_count = ba.internal.get_v1_account_ticket_count() except Exception: # FIXME: should add a ba.NotSignedInError we can use here. ticket_count = None @@ -804,7 +806,7 @@ class PrivateGatherTab(GatherTab): ba.playsound(ba.getsound('error')) return self._last_action_send_time = time.time() - _ba.add_transaction( + ba.internal.add_transaction( { 'type': 'PRIVATE_PARTY_START', 'config': dataclass_to_dict(self._hostingconfig), @@ -812,17 +814,17 @@ class PrivateGatherTab(GatherTab): 'expire_time': time.time() + 20, }, callback=ba.WeakCall(self._hosting_state_response)) - _ba.run_transactions() + ba.internal.run_transactions() else: self._last_action_send_time = time.time() - _ba.add_transaction( + ba.internal.add_transaction( { 'type': 'PRIVATE_PARTY_STOP', 'expire_time': time.time() + 20, }, callback=ba.WeakCall(self._hosting_state_response)) - _ba.run_transactions() + ba.internal.run_transactions() ba.playsound(ba.getsound('click01')) self._waiting_for_start_stop_response = True @@ -860,7 +862,7 @@ class PrivateGatherTab(GatherTab): return self._debug_server_comm('got valid connect response') assert cresult.addr is not None and cresult.port is not None - _ba.connect_to_party(cresult.addr, port=cresult.port) + ba.internal.connect_to_party(cresult.addr, port=cresult.port) except Exception: self._debug_server_comm('got connect response error') ba.playsound(ba.getsound('error')) diff --git a/dist/ba_data/python/bastd/ui/gather/publictab.py b/dist/ba_data/python/bastd/ui/gather/publictab.py index 726cf9e..522c9e3 100644 --- a/dist/ba_data/python/bastd/ui/gather/publictab.py +++ b/dist/ba_data/python/bastd/ui/gather/publictab.py @@ -12,8 +12,8 @@ from enum import Enum from dataclasses import dataclass from typing import TYPE_CHECKING, cast -import _ba import ba +import ba.internal from bastd.ui.gather import GatherTab if TYPE_CHECKING: @@ -88,8 +88,8 @@ class UIRow: if party.clean_display_index == index: return - ping_good = _ba.get_v1_account_misc_read_val('pingGood', 100) - ping_med = _ba.get_v1_account_misc_read_val('pingMed', 500) + ping_good = ba.internal.get_v1_account_misc_read_val('pingGood', 100) + ping_med = ba.internal.get_v1_account_misc_read_val('pingMed', 500) self._clear() hpos = 20 @@ -122,8 +122,8 @@ class UIRow: if party.stats_addr: url = party.stats_addr.replace( '${ACCOUNT}', - _ba.get_v1_account_misc_read_val_2('resolvedAccountID', - 'UNKNOWN')) + ba.internal.get_v1_account_misc_read_val_2( + 'resolvedAccountID', 'UNKNOWN')) self._stats_button = ba.buttonwidget( color=(0.3, 0.6, 0.94), textcolor=(1.0, 1.0, 1.0), @@ -582,7 +582,7 @@ class PublicGatherTab(GatherTab): c_height = region_height - 20 v = c_height - 35 v -= 25 - is_public_enabled = _ba.get_public_party_enabled() + is_public_enabled = ba.internal.get_public_party_enabled() v -= 30 ba.textwidget( @@ -643,7 +643,7 @@ class PublicGatherTab(GatherTab): scale=1.2, color=(1, 1, 1), position=(240, v - 9), - text=str(_ba.get_public_party_max_size())) + text=str(ba.internal.get_public_party_max_size())) btn1 = self._host_max_party_size_minus_button = (ba.buttonwidget( parent=self._container, size=(40, 40), @@ -711,7 +711,7 @@ class PublicGatherTab(GatherTab): # If public sharing is already on, # launch a status-check immediately. - if _ba.get_public_party_enabled(): + if ba.internal.get_public_party_enabled(): self._do_status_check() def _on_public_party_query_result(self, @@ -793,7 +793,7 @@ class PublicGatherTab(GatherTab): self._process_pending_party_infos() # Anytime we sign in/out, make sure we refresh our list. - signed_in = _ba.get_v1_account_state() == 'signed_in' + signed_in = ba.internal.get_v1_account_state() == 'signed_in' if self._signed_in != signed_in: self._signed_in = signed_in self._party_lists_dirty = True @@ -806,7 +806,7 @@ class PublicGatherTab(GatherTab): text = self._host_name_text if text: name = cast(str, ba.textwidget(query=self._host_name_text)) - _ba.set_public_party_name(name) + ba.internal.set_public_party_name(name) # Update status text. status_text = self._join_status_text @@ -986,7 +986,7 @@ class PublicGatherTab(GatherTab): p[1].index)) # If signed out or errored, show no parties. - if (_ba.get_v1_account_state() != 'signed_in' + if (ba.internal.get_v1_account_state() != 'signed_in' or not self._have_valid_server_list): self._parties_displayed = {} else: @@ -1022,20 +1022,21 @@ class PublicGatherTab(GatherTab): # Fire off a new public-party query periodically. if (self._last_server_list_query_time is None - or now - self._last_server_list_query_time > 0.001 * - _ba.get_v1_account_misc_read_val('pubPartyRefreshMS', 10000)): + or now - self._last_server_list_query_time > + 0.001 * ba.internal.get_v1_account_misc_read_val( + 'pubPartyRefreshMS', 10000)): self._last_server_list_query_time = now if DEBUG_SERVER_COMMUNICATION: print('REQUESTING SERVER LIST') - if _ba.get_v1_account_state() == 'signed_in': - _ba.add_transaction( + if ba.internal.get_v1_account_state() == 'signed_in': + ba.internal.add_transaction( { 'type': 'PUBLIC_PARTY_QUERY', 'proto': ba.app.protocol_version, 'lang': ba.app.lang.language }, callback=ba.WeakCall(self._on_public_party_query_result)) - _ba.run_transactions() + ba.internal.run_transactions() else: self._on_public_party_query_result(None) @@ -1128,15 +1129,18 @@ class PublicGatherTab(GatherTab): edit=text, text=ba.Lstr( value='${A}\n${B}${C}', - subs=[('${A}', - ba.Lstr(resource='gatherWindow.' - 'partyStatusNotJoinableText')), - ('${B}', - ba.Lstr(resource='gatherWindow.' - 'manualRouterForwardingText', - subs=[('${PORT}', - str(_ba.get_game_port()))])), - ('${C}', ex_line)]), + subs=[ + ('${A}', + ba.Lstr(resource='gatherWindow.' + 'partyStatusNotJoinableText')), + ('${B}', + ba.Lstr(resource='gatherWindow.' + 'manualRouterForwardingText', + subs=[ + ('${PORT}', + str(ba.internal.get_game_port())) + ])), ('${C}', ex_line) + ]), color=(1, 0, 0)) else: ba.textwidget(edit=text, @@ -1156,7 +1160,7 @@ class PublicGatherTab(GatherTab): def _on_start_advertizing_press(self) -> None: from bastd.ui.account import show_sign_in_prompt - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return @@ -1166,16 +1170,16 @@ class PublicGatherTab(GatherTab): color=(1, 0, 0)) ba.playsound(ba.getsound('error')) return - _ba.set_public_party_name(name) + ba.internal.set_public_party_name(name) cfg = ba.app.config cfg['Public Party Name'] = name cfg.commit() ba.playsound(ba.getsound('shieldUp')) - _ba.set_public_party_enabled(True) + ba.internal.set_public_party_enabled(True) # In GUI builds we want to authenticate clients only when hosting # public parties. - _ba.set_authenticate_clients(True) + ba.internal.set_authenticate_clients(True) self._do_status_check() ba.buttonwidget( @@ -1186,11 +1190,11 @@ class PublicGatherTab(GatherTab): on_activate_call=self._on_stop_advertising_press) def _on_stop_advertising_press(self) -> None: - _ba.set_public_party_enabled(False) + ba.internal.set_public_party_enabled(False) # In GUI builds we want to authenticate clients only when hosting # public parties. - _ba.set_authenticate_clients(False) + ba.internal.set_authenticate_clients(False) ba.playsound(ba.getsound('shieldDown')) text = self._host_status_text if text: @@ -1222,7 +1226,7 @@ class PublicGatherTab(GatherTab): now = time.time() last_connect_time = self._last_connect_attempt_time if last_connect_time is None or now - last_connect_time > 2.0: - _ba.connect_to_party(address, port=port) + ba.internal.connect_to_party(address, port=port) self._last_connect_attempt_time = now def set_public_party_selection(self, sel: Selection) -> None: @@ -1233,12 +1237,12 @@ class PublicGatherTab(GatherTab): self._have_user_selected_row = True def _on_max_public_party_size_minus_press(self) -> None: - val = max(1, _ba.get_public_party_max_size() - 1) - _ba.set_public_party_max_size(val) + val = max(1, ba.internal.get_public_party_max_size() - 1) + ba.internal.set_public_party_max_size(val) ba.textwidget(edit=self._host_max_party_size_value, text=str(val)) def _on_max_public_party_size_plus_press(self) -> None: - val = _ba.get_public_party_max_size() + val = ba.internal.get_public_party_max_size() val += 1 - _ba.set_public_party_max_size(val) + ba.internal.set_public_party_max_size(val) ba.textwidget(edit=self._host_max_party_size_value, text=str(val)) diff --git a/dist/ba_data/python/bastd/ui/getcurrency.py b/dist/ba_data/python/bastd/ui/getcurrency.py index bc16517..549fec4 100644 --- a/dist/ba_data/python/bastd/ui/getcurrency.py +++ b/dist/ba_data/python/bastd/ui/getcurrency.py @@ -6,8 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: from typing import Any @@ -176,38 +176,34 @@ class GetCurrencyWindow(ba.Window): rsrc = self._r + '.ticketsText' - c2txt = ba.Lstr( - resource=rsrc, - subs=[('${COUNT}', - str(_ba.get_v1_account_misc_read_val('tickets2Amount', - 500)))]) - c3txt = ba.Lstr( - resource=rsrc, - subs=[ - ('${COUNT}', - str(_ba.get_v1_account_misc_read_val('tickets3Amount', 1500))) - ]) - c4txt = ba.Lstr( - resource=rsrc, - subs=[ - ('${COUNT}', - str(_ba.get_v1_account_misc_read_val('tickets4Amount', 5000))) - ]) - c5txt = ba.Lstr( - resource=rsrc, - subs=[ - ('${COUNT}', - str(_ba.get_v1_account_misc_read_val('tickets5Amount', - 15000))) - ]) + c2txt = ba.Lstr(resource=rsrc, + subs=[('${COUNT}', + str( + ba.internal.get_v1_account_misc_read_val( + 'tickets2Amount', 500)))]) + c3txt = ba.Lstr(resource=rsrc, + subs=[('${COUNT}', + str( + ba.internal.get_v1_account_misc_read_val( + 'tickets3Amount', 1500)))]) + c4txt = ba.Lstr(resource=rsrc, + subs=[('${COUNT}', + str( + ba.internal.get_v1_account_misc_read_val( + 'tickets4Amount', 5000)))]) + c5txt = ba.Lstr(resource=rsrc, + subs=[('${COUNT}', + str( + ba.internal.get_v1_account_misc_read_val( + 'tickets5Amount', 15000)))]) h = 110.0 # enable buttons if we have prices.. - tickets2_price = _ba.get_price('tickets2') - tickets3_price = _ba.get_price('tickets3') - tickets4_price = _ba.get_price('tickets4') - tickets5_price = _ba.get_price('tickets5') + tickets2_price = ba.internal.get_price('tickets2') + tickets3_price = ba.internal.get_price('tickets3') + tickets4_price = ba.internal.get_price('tickets4') + tickets5_price = ba.internal.get_price('tickets5') # TEMP # tickets1_price = '$0.99' @@ -252,7 +248,7 @@ class GetCurrencyWindow(ba.Window): tex_name='ticketRolls', tex_scale=1.2) # 19.99-ish - self._enable_ad_button = _ba.has_video_ads() + self._enable_ad_button = ba.internal.has_video_ads() h = self._width * 0.5 + 110.0 v = self._height - b_size[1] - 115.0 @@ -263,11 +259,12 @@ class GetCurrencyWindow(ba.Window): 'ad', position=(h + h_offs, v), size=b_size_3, - label=ba.Lstr(resource=self._r + '.ticketsFromASponsorText', - subs=[('${COUNT}', - str( - _ba.get_v1_account_misc_read_val( - 'sponsorTickets', 5)))]), + label=ba.Lstr( + resource=self._r + '.ticketsFromASponsorText', + subs=[('${COUNT}', + str( + ba.internal.get_v1_account_misc_read_val( + 'sponsorTickets', 5)))]), tex_name='ticketsMore', enabled=self._enable_ad_button, tex_opacity=0.6, @@ -308,7 +305,7 @@ class GetCurrencyWindow(ba.Window): resource='gatherWindow.earnTicketsForRecommendingText', subs=[('${COUNT}', str( - _ba.get_v1_account_misc_read_val( + ba.internal.get_v1_account_misc_read_val( 'sponsorTickets', 5)))]), tex_name='ticketsMore', enabled=True, @@ -431,22 +428,22 @@ class GetCurrencyWindow(ba.Window): import datetime # if we somehow get signed out, just die.. - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': self._back() return - self._ticket_count = _ba.get_v1_account_ticket_count() + self._ticket_count = ba.internal.get_v1_account_ticket_count() # update our incentivized ad button depending on whether ads are # available if self._ad_button is not None: - next_reward_ad_time = _ba.get_v1_account_misc_read_val_2( + next_reward_ad_time = ba.internal.get_v1_account_misc_read_val_2( 'nextRewardAdTime', None) if next_reward_ad_time is not None: next_reward_ad_time = datetime.datetime.utcfromtimestamp( next_reward_ad_time) now = datetime.datetime.utcnow() - if (_ba.have_incentivized_ad() and + if (ba.internal.have_incentivized_ad() and (next_reward_ad_time is None or next_reward_ad_time <= now)): self._ad_button_greyed = False ba.buttonwidget(edit=self._ad_button, color=(0.65, 0.5, 0.7)) @@ -499,8 +496,8 @@ class GetCurrencyWindow(ba.Window): if ((app.test_build or (app.platform == 'android' and app.subplatform in ['oculus', 'cardboard'])) - and _ba.get_v1_account_misc_read_val('allowAccountLinking2', - False)): + and ba.internal.get_v1_account_misc_read_val( + 'allowAccountLinking2', False)): ba.screenmessage(ba.Lstr(resource=self._r + '.unavailableLinkAccountText'), color=(1, 0.5, 0)) @@ -514,7 +511,7 @@ class GetCurrencyWindow(ba.Window): from bastd.ui import appinvite from ba.internal import master_server_get if item == 'app_invite': - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() return appinvite.handle_app_invites_press() @@ -559,7 +556,7 @@ class GetCurrencyWindow(ba.Window): if item == 'ad': import datetime # if ads are disabled until some time, error.. - next_reward_ad_time = _ba.get_v1_account_misc_read_val_2( + next_reward_ad_time = ba.internal.get_v1_account_misc_read_val_2( 'nextRewardAdTime', None) if next_reward_ad_time is not None: next_reward_ad_time = datetime.datetime.utcfromtimestamp( @@ -572,9 +569,9 @@ class GetCurrencyWindow(ba.Window): resource='getTicketsWindow.unavailableTemporarilyText'), color=(1, 0, 0)) elif self._enable_ad_button: - _ba.app.ads.show_ad('tickets') + ba.app.ads.show_ad('tickets') else: - _ba.purchase(item) + ba.internal.purchase(item) def _back(self) -> None: from bastd.ui.store import browser diff --git a/dist/ba_data/python/bastd/ui/helpui.py b/dist/ba_data/python/bastd/ui/helpui.py index 79369a9..8983640 100644 --- a/dist/ba_data/python/bastd/ui/helpui.py +++ b/dist/ba_data/python/bastd/ui/helpui.py @@ -6,8 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: pass @@ -76,8 +76,9 @@ class HelpWindow(ba.Window): capture_arrows=True) if ba.app.ui.use_toolbars: - ba.widget(edit=self._scrollwidget, - right_widget=_ba.get_special_widget('party_button')) + ba.widget( + edit=self._scrollwidget, + right_widget=ba.internal.get_special_widget('party_button')) ba.containerwidget(edit=self._root_widget, selected_child=self._scrollwidget) @@ -86,8 +87,9 @@ class HelpWindow(ba.Window): if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: ba.containerwidget(edit=self._root_widget, on_cancel_call=self._close) - ba.widget(edit=self._scrollwidget, - left_widget=_ba.get_special_widget('back_button')) + ba.widget( + edit=self._scrollwidget, + left_widget=ba.internal.get_special_widget('back_button')) else: btn = ba.buttonwidget( parent=self._root_widget, @@ -150,7 +152,8 @@ class HelpWindow(ba.Window): maxwidth=txt_maxwidth) txt_width = min( txt_maxwidth, - _ba.get_string_width(txt, suppress_warning=True) * txt_scale) + ba.internal.get_string_width(txt, suppress_warning=True) * + txt_scale) icon_size = 70 hval2 = h - (txt_width * 0.5 + icon_size * 0.5 * icon_buffer) @@ -350,7 +353,8 @@ class HelpWindow(ba.Window): maxwidth=txt_maxwidth) txt_width = min( txt_maxwidth, - _ba.get_string_width(txt, suppress_warning=True) * txt_scale) + ba.internal.get_string_width(txt, suppress_warning=True) * + txt_scale) icon_size = 70 hval2 = h - (txt_width * 0.5 + icon_size * 0.5 * icon_buffer) @@ -496,7 +500,8 @@ class HelpWindow(ba.Window): maxwidth=txt_maxwidth) txt_width = min( txt_maxwidth, - _ba.get_string_width(txt, suppress_warning=True) * txt_scale) + ba.internal.get_string_width(txt, suppress_warning=True) * + txt_scale) icon_size = 70 hval2 = h - (txt_width * 0.5 + icon_size * 0.5 * icon_buffer) ba.imagewidget(parent=self._subcontainer, diff --git a/dist/ba_data/python/bastd/ui/iconpicker.py b/dist/ba_data/python/bastd/ui/iconpicker.py index 9bd98fe..dbe109b 100644 --- a/dist/ba_data/python/bastd/ui/iconpicker.py +++ b/dist/ba_data/python/bastd/ui/iconpicker.py @@ -7,8 +7,8 @@ from __future__ import annotations import math from typing import TYPE_CHECKING -import _ba import ba +import ba.internal from bastd.ui import popup if TYPE_CHECKING: @@ -137,7 +137,7 @@ class IconPicker(popup.PopupWindow): def _on_store_press(self) -> None: from bastd.ui.account import show_sign_in_prompt from bastd.ui.store.browser import StoreBrowserWindow - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return self._transition_out() diff --git a/dist/ba_data/python/bastd/ui/kiosk.py b/dist/ba_data/python/bastd/ui/kiosk.py index c748b60..73fadc9 100644 --- a/dist/ba_data/python/bastd/ui/kiosk.py +++ b/dist/ba_data/python/bastd/ui/kiosk.py @@ -6,8 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: pass @@ -37,7 +37,7 @@ class KioskWindow(ba.Window): self._show_multiplayer = False # Let's reset all random player names every time we hit the main menu. - _ba.reset_random_player_names() + ba.internal.reset_random_player_names() # Reset achievements too (at least locally). ba.app.config['Achievements'] = {} @@ -360,7 +360,7 @@ class KioskWindow(ba.Window): def _update(self) -> None: # Kiosk-mode is designed to be used signed-out... try for force # the issue. - if _ba.get_v1_account_state() == 'signed_in': + if ba.internal.get_v1_account_state() == 'signed_in': # _bs.sign_out() # FIXME: Try to delete player profiles here too. pass @@ -390,11 +390,12 @@ class KioskWindow(ba.Window): 'type': 'bs_elimination.EliminationGame' }] appconfig['Free-for-All Playlist Selection'] = 'Just Epic Elim' - _ba.fade_screen(False, - endcall=ba.Call( - ba.pushcall, - ba.Call(_ba.new_host_session, - ba.FreeForAllSession))) + ba.internal.fade_screen(False, + endcall=ba.Call( + ba.pushcall, + ba.Call( + ba.internal.new_host_session, + ba.FreeForAllSession))) else: if mode == 'ctf': appconfig['Team Tournament Playlists']['Just CTF'] = [{ @@ -423,11 +424,12 @@ class KioskWindow(ba.Window): }] appconfig['Team Tournament Playlist Selection'] = ( 'Just Hockey') - _ba.fade_screen(False, - endcall=ba.Call( - ba.pushcall, - ba.Call(_ba.new_host_session, - ba.DualTeamSession))) + ba.internal.fade_screen(False, + endcall=ba.Call( + ba.pushcall, + ba.Call( + ba.internal.new_host_session, + ba.DualTeamSession))) ba.containerwidget(edit=self._root_widget, transition='out_left') return diff --git a/dist/ba_data/python/bastd/ui/league/rankbutton.py b/dist/ba_data/python/bastd/ui/league/rankbutton.py index 8509eb8..a679b8a 100644 --- a/dist/ba_data/python/bastd/ui/league/rankbutton.py +++ b/dist/ba_data/python/bastd/ui/league/rankbutton.py @@ -6,8 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: from typing import Any, Callable @@ -94,7 +94,7 @@ class LeagueRankButton: self._smooth_update_timer: ba.Timer | None = None # Take note of our account state; we'll refresh later if this changes. - self._account_state_num = _ba.get_v1_account_state_num() + self._account_state_num = ba.internal.get_v1_account_state_num() self._last_power_ranking_query_time: float | None = None self._doing_power_ranking_query = False self.set_position(position) @@ -111,7 +111,7 @@ class LeagueRankButton: self._update_for_league_rank_data(data) def _on_activate(self) -> None: - _ba.increment_analytics_count('League rank button press') + ba.internal.increment_analytics_count('League rank button press') self._on_activate_call() def __del__(self) -> None: @@ -224,7 +224,7 @@ class LeagueRankButton: in_top = data is not None and data['rank'] is not None do_percent = False - if data is None or _ba.get_v1_account_state() != 'signed_in': + if data is None or ba.internal.get_v1_account_state() != 'signed_in': self._percent = self._rank = None status_text = '-' elif in_top: @@ -335,7 +335,7 @@ class LeagueRankButton: cur_time = ba.time(ba.TimeType.REAL) # If our account state has changed, refresh our UI. - account_state_num = _ba.get_v1_account_state_num() + account_state_num = ba.internal.get_v1_account_state_num() if account_state_num != self._account_state_num: self._account_state_num = account_state_num @@ -350,7 +350,7 @@ class LeagueRankButton: or cur_time - self._last_power_ranking_query_time > 30.0): self._last_power_ranking_query_time = cur_time self._doing_power_ranking_query = True - _ba.power_ranking_query( + ba.internal.power_ranking_query( callback=ba.WeakCall(self._on_power_ranking_query_response)) def _default_on_activate_call(self) -> None: diff --git a/dist/ba_data/python/bastd/ui/league/rankwindow.py b/dist/ba_data/python/bastd/ui/league/rankwindow.py index a29f618..6c6dab4 100644 --- a/dist/ba_data/python/bastd/ui/league/rankwindow.py +++ b/dist/ba_data/python/bastd/ui/league/rankwindow.py @@ -7,8 +7,8 @@ from __future__ import annotations import copy from typing import TYPE_CHECKING -import _ba import ba +import ba.internal from bastd.ui import popup as popup_ui if TYPE_CHECKING: @@ -118,7 +118,7 @@ class LeagueRankWindow(ba.Window): self._season: str | None = None # take note of our account state; we'll refresh later if this changes - self._account_state = _ba.get_v1_account_state() + self._account_state = ba.internal.get_v1_account_state() self._refresh() self._restore_state() @@ -155,8 +155,9 @@ class LeagueRankWindow(ba.Window): resource='coopSelectWindow.activenessAllTimeInfoText' if self._season == 'a' else 'coopSelectWindow.activenessInfoText', subs=[('${MAX}', - str(_ba.get_v1_account_misc_read_val('activenessMax', - 1.0)))]) + str( + ba.internal.get_v1_account_misc_read_val( + 'activenessMax', 1.0)))]) confirm.ConfirmWindow(txt, cancel_button=False, width=460, @@ -168,7 +169,7 @@ class LeagueRankWindow(ba.Window): txt = ba.Lstr(resource='coopSelectWindow.proMultInfoText', subs=[('${PERCENT}', str( - _ba.get_v1_account_misc_read_val( + ba.internal.get_v1_account_misc_read_val( 'proPowerRankingBoost', 10))), ('${PRO}', ba.Lstr(resource='store.bombSquadProNameText', @@ -208,7 +209,7 @@ class LeagueRankWindow(ba.Window): cur_time = ba.time(ba.TimeType.REAL) # if our account state has changed, refresh our UI - account_state = _ba.get_v1_account_state() + account_state = ba.internal.get_v1_account_state() if account_state != self._account_state: self._account_state = account_state self._save_state() @@ -242,9 +243,9 @@ class LeagueRankWindow(ba.Window): self._last_power_ranking_query_time = cur_time self._doing_power_ranking_query = True - _ba.power_ranking_query(season=self._requested_season, - callback=ba.WeakCall( - self._on_power_ranking_query_response)) + ba.internal.power_ranking_query( + season=self._requested_season, + callback=ba.WeakCall(self._on_power_ranking_query_response)) def _refresh(self) -> None: # pylint: disable=too-many-statements @@ -352,7 +353,7 @@ class LeagueRankWindow(ba.Window): maxwidth=200) self._activity_mult_button: ba.Widget | None - if _ba.get_v1_account_misc_read_val('act', False): + if ba.internal.get_v1_account_misc_read_val('act', False): self._activity_mult_button = ba.buttonwidget( parent=w_parent, position=(h2 - 60, v2 + 10), @@ -564,7 +565,7 @@ class LeagueRankWindow(ba.Window): self._on_more_press)) def _on_more_press(self) -> None: - our_login_id = _ba.get_public_login_id() + our_login_id = ba.internal.get_public_login_id() # our_login_id = _bs.get_account_misc_read_val_2( # 'resolvedAccountID', None) if not self._can_do_more_button or our_login_id is None: @@ -582,7 +583,7 @@ class LeagueRankWindow(ba.Window): league_str = '&league=' + self._league_url_arg else: league_str = '' - ba.open_url(_ba.get_master_server_address() + + ba.open_url(ba.internal.get_master_server_address() + '/highscores?list=powerRankings&v=2' + league_str + season_str + '&player=' + our_login_id) @@ -602,7 +603,7 @@ class LeagueRankWindow(ba.Window): finished_season_unranked = False self._can_do_more_button = True extra_text = '' - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': status_text = '(' + ba.Lstr( resource='notSignedInText').evaluate() + ')' elif in_top: @@ -746,7 +747,7 @@ class LeagueRankWindow(ba.Window): ba.textwidget(edit=self._league_text, text=lname, color=lcolor) l_text_width = min( self._league_text_maxwidth, - _ba.get_string_width(lname, suppress_warning=True) * + ba.internal.get_string_width(lname, suppress_warning=True) * self._league_text_scale) ba.textwidget( edit=self._league_number_text, @@ -789,8 +790,8 @@ class LeagueRankWindow(ba.Window): have_pro = False if data is None else data['p'] pro_mult = 1.0 + float( - _ba.get_v1_account_misc_read_val('proPowerRankingBoost', - 0.0)) * 0.01 + ba.internal.get_v1_account_misc_read_val('proPowerRankingBoost', + 0.0)) * 0.01 # pylint: disable=consider-using-f-string ba.textwidget(edit=self._pro_mult_text, text=' -' if diff --git a/dist/ba_data/python/bastd/ui/mainmenu.py b/dist/ba_data/python/bastd/ui/mainmenu.py index 4e5c8c5..6e8ad23 100644 --- a/dist/ba_data/python/bastd/ui/mainmenu.py +++ b/dist/ba_data/python/bastd/ui/mainmenu.py @@ -8,7 +8,7 @@ from __future__ import annotations from typing import TYPE_CHECKING import ba -import _ba +import ba.internal if TYPE_CHECKING: from typing import Any, Callable @@ -21,8 +21,8 @@ class MainMenuWindow(ba.Window): # pylint: disable=cyclic-import import threading from bastd.mainmenu import MainMenuSession - self._in_game = not isinstance(_ba.get_foreground_host_session(), - MainMenuSession) + self._in_game = not isinstance( + ba.internal.get_foreground_host_session(), MainMenuSession) # Preload some modules we use in a background thread so we won't # have a visual hitch when the user taps them. @@ -67,9 +67,9 @@ class MainMenuWindow(ba.Window): self._restore_state() # Keep an eye on a few things and refresh if they change. - self._account_state = _ba.get_v1_account_state() - self._account_state_num = _ba.get_v1_account_state_num() - self._account_type = (_ba.get_v1_account_type() + self._account_state = ba.internal.get_v1_account_state() + self._account_state_num = ba.internal.get_v1_account_state_num() + self._account_type = (ba.internal.get_v1_account_type() if self._account_state == 'signed_in' else None) self._refresh_timer = ba.Timer(1.0, ba.WeakCall(self._check_refresh), @@ -101,7 +101,7 @@ class MainMenuWindow(ba.Window): try: app = ba.app force_test = False - _ba.get_local_active_input_devices_count() + ba.internal.get_local_active_input_devices_count() if (((app.on_tv or app.platform == 'mac') and ba.app.config.get('launchCount', 0) <= 1) or force_test): @@ -122,10 +122,11 @@ class MainMenuWindow(ba.Window): ba.print_exception('Error showing get-remote-app info') def _get_store_char_tex(self) -> str: - return ('storeCharacterXmas' if _ba.get_v1_account_misc_read_val( - 'xmas', False) else - 'storeCharacterEaster' if _ba.get_v1_account_misc_read_val( - 'easter', False) else 'storeCharacter') + return ( + 'storeCharacterXmas' if ba.internal.get_v1_account_misc_read_val( + 'xmas', False) else + 'storeCharacterEaster' if ba.internal.get_v1_account_misc_read_val( + 'easter', False) else 'storeCharacter') def _check_refresh(self) -> None: if not self._root_widget: @@ -138,13 +139,14 @@ class MainMenuWindow(ba.Window): return store_char_tex = self._get_store_char_tex() - account_state_num = _ba.get_v1_account_state_num() + account_state_num = ba.internal.get_v1_account_state_num() if (account_state_num != self._account_state_num or store_char_tex != self._store_char_tex): self._store_char_tex = store_char_tex self._account_state_num = account_state_num - account_state = self._account_state = (_ba.get_v1_account_state()) - self._account_type = (_ba.get_v1_account_type() + account_state = self._account_state = ( + ba.internal.get_v1_account_state()) + self._account_type = (ba.internal.get_v1_account_type() if account_state == 'signed_in' else None) self._save_state() self._refresh() @@ -185,7 +187,7 @@ class MainMenuWindow(ba.Window): (not self._in_game or not app.toolbar_test) and not (self._is_demo or self._is_arcade or self._is_iircade)) - self._input_device = input_device = _ba.get_ui_input_device() + self._input_device = input_device = ba.internal.get_ui_input_device() self._input_player = input_device.player if input_device else None self._connected_to_remote_player = ( input_device.is_connected_to_remote_player() @@ -213,8 +215,8 @@ class MainMenuWindow(ba.Window): on_activate_call=self._settings) # Scattered eggs on easter. - if _ba.get_v1_account_misc_read_val('easter', - False) and not self._in_game: + if ba.internal.get_v1_account_misc_read_val( + 'easter', False) and not self._in_game: icon_size = 34 ba.imagewidget(parent=self._root_widget, position=(h - icon_size * 0.5 - 15, @@ -232,7 +234,7 @@ class MainMenuWindow(ba.Window): self._p_index += 1 # If we're in a replay, we have a 'Leave Replay' button. - if _ba.is_in_replay(): + if ba.internal.is_in_replay(): ba.buttonwidget(parent=self._root_widget, position=(h - self._button_width * 0.5 * scale, v), @@ -241,7 +243,7 @@ class MainMenuWindow(ba.Window): autoselect=self._use_autoselect, label=ba.Lstr(resource='replayEndText'), on_activate_call=self._confirm_end_replay) - elif _ba.get_foreground_host_session() is not None: + elif ba.internal.get_foreground_host_session() is not None: ba.buttonwidget( parent=self._root_widget, position=(h - self._button_width * 0.5 * scale, v), @@ -310,7 +312,7 @@ class MainMenuWindow(ba.Window): transition_delay=self._tdelay) # Scattered eggs on easter. - if _ba.get_v1_account_misc_read_val('easter', False): + if ba.internal.get_v1_account_misc_read_val('easter', False): icon_size = 30 ba.imagewidget(parent=self._root_widget, position=(h - icon_size * 0.5 + 25, @@ -341,7 +343,7 @@ class MainMenuWindow(ba.Window): # Add speed-up/slow-down buttons for replays. # (ideally this should be part of a fading-out playback bar like most # media players but this works for now). - if _ba.is_in_replay(): + if ba.internal.is_in_replay(): b_size = 50.0 b_buffer = 10.0 t_scale = 0.75 @@ -427,8 +429,8 @@ class MainMenuWindow(ba.Window): self._height = 200.0 enable_account_button = True account_type_name: str | ba.Lstr - if _ba.get_v1_account_state() == 'signed_in': - account_type_name = _ba.get_v1_account_display_string() + if ba.internal.get_v1_account_state() == 'signed_in': + account_type_name = ba.internal.get_v1_account_display_string() account_type_icon = None account_textcolor = (1.0, 1.0, 1.0) else: @@ -618,8 +620,8 @@ class MainMenuWindow(ba.Window): enable_sound=account_type_enable_button_sound) # Scattered eggs on easter. - if _ba.get_v1_account_misc_read_val('easter', - False) and not self._in_game: + if ba.internal.get_v1_account_misc_read_val( + 'easter', False) and not self._in_game: icon_size = 32 ba.imagewidget(parent=self._root_widget, position=(h - icon_size * 0.5 + 35, @@ -648,8 +650,8 @@ class MainMenuWindow(ba.Window): self._how_to_play_button = btn # Scattered eggs on easter. - if _ba.get_v1_account_misc_read_val('easter', - False) and not self._in_game: + if ba.internal.get_v1_account_misc_read_val( + 'easter', False) and not self._in_game: icon_size = 28 ba.imagewidget(parent=self._root_widget, position=(h - icon_size * 0.5 + 30, @@ -682,7 +684,7 @@ class MainMenuWindow(ba.Window): # pylint: disable=too-many-locals # pylint: disable=too-many-statements custom_menu_entries: list[dict[str, Any]] = [] - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() if session is not None: try: custom_menu_entries = session.get_custom_menu_entries() @@ -819,8 +821,9 @@ class MainMenuWindow(ba.Window): if ba.do_once(): print('_change_replay_speed called without widget') return - _ba.set_replay_speed_exponent(_ba.get_replay_speed_exponent() + offs) - actual_speed = pow(2.0, _ba.get_replay_speed_exponent()) + ba.internal.set_replay_speed_exponent( + ba.internal.get_replay_speed_exponent() + offs) + actual_speed = pow(2.0, ba.internal.get_replay_speed_exponent()) ba.textwidget(edit=self._replay_speed_text, text=ba.Lstr(resource='watchWindow.playbackSpeedText', subs=[('${SPEED}', str(actual_speed))])) @@ -851,7 +854,7 @@ class MainMenuWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui.store.browser import StoreBrowserWindow from bastd.ui.account import show_sign_in_prompt - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return self._save_state() @@ -892,7 +895,7 @@ class MainMenuWindow(ba.Window): cancel_is_selected=True) def _leave_party(self) -> None: - _ba.disconnect_from_host() + ba.internal.disconnect_from_host() def _end_game(self) -> None: if not self._root_widget: @@ -942,7 +945,7 @@ class MainMenuWindow(ba.Window): def _do_game_service_press(self) -> None: self._save_state() - _ba.show_online_score_ui() + ba.internal.show_online_score_ui() def _save_state(self) -> None: diff --git a/dist/ba_data/python/bastd/ui/onscreenkeyboard.py b/dist/ba_data/python/bastd/ui/onscreenkeyboard.py index 69d2303..842ecd1 100644 --- a/dist/ba_data/python/bastd/ui/onscreenkeyboard.py +++ b/dist/ba_data/python/bastd/ui/onscreenkeyboard.py @@ -6,10 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING, cast -import _ba import ba -from ba import charstr -from ba import SpecialChar as SpCh +import ba.internal if TYPE_CHECKING: pass @@ -25,7 +23,7 @@ class OnScreenKeyboardWindow(ba.Window): uiscale = ba.app.ui.uiscale top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 super().__init__(root_widget=ba.containerwidget( - parent=_ba.get_special_widget('overlay_stack'), + parent=ba.internal.get_special_widget('overlay_stack'), size=(self._width, self._height + top_extra), transition='in_scale', scale_origin_stack_offset=self._target_text. @@ -129,7 +127,7 @@ class OnScreenKeyboardWindow(ba.Window): autoselect=True, textcolor=key_textcolor, color=key_color_dark, - label=charstr(SpCh.SHIFT), + label=ba.charstr(ba.SpecialChar.SHIFT), enable_sound=False, extra_touch_border_scale=0.3, button_type='square', @@ -165,7 +163,7 @@ class OnScreenKeyboardWindow(ba.Window): repeat=True, textcolor=key_textcolor, color=key_color_dark, - label=charstr(SpCh.DELETE), + label=ba.charstr(ba.SpecialChar.DELETE), button_type='square', on_activate_call=self._del) v -= (key_height + 9) @@ -193,7 +191,7 @@ class OnScreenKeyboardWindow(ba.Window): enable_sound=False, textcolor=key_textcolor, color=key_color_dark, - label=charstr(SpCh.LOGO_FLAT), + label=ba.charstr(ba.SpecialChar.LOGO_FLAT), extra_touch_border_scale=0.3, button_type='square', ) @@ -213,8 +211,10 @@ class OnScreenKeyboardWindow(ba.Window): # Show change instructions only if we have more than one # keyboard option. - if (ba.app.meta.scanresults is not None - and len(ba.app.meta.scanresults.keyboards) > 1): + keyboards = (ba.app.meta.scanresults.exports_of_class( + ba.Keyboard) if ba.app.meta.scanresults is not None + else []) + if len(keyboards) > 1: ba.textwidget( parent=self._root_widget, h_align='center', @@ -239,7 +239,8 @@ class OnScreenKeyboardWindow(ba.Window): def _get_keyboard(self) -> ba.Keyboard: assert ba.app.meta.scanresults is not None - classname = ba.app.meta.scanresults.keyboards[self._keyboard_index] + classname = ba.app.meta.scanresults.exports_of_class( + ba.Keyboard)[self._keyboard_index] kbclass = ba.getclass(classname, ba.Keyboard) return kbclass() @@ -252,14 +253,14 @@ class OnScreenKeyboardWindow(ba.Window): ba.buttonwidget(edit=self._shift_button, color=self._key_color_lit if self._mode == 'caps' else self._key_color_dark, - label=charstr(SpCh.SHIFT), + label=ba.charstr(ba.SpecialChar.SHIFT), on_activate_call=self._shift) ba.buttonwidget(edit=self._num_mode_button, label='123#&*', on_activate_call=self._num_mode) ba.buttonwidget(edit=self._emoji_button, color=self._key_color_dark, - label=charstr(SpCh.LOGO_FLAT), + label=ba.charstr(ba.SpecialChar.LOGO_FLAT), on_activate_call=self._next_mode) else: if self._mode == 'num': @@ -275,7 +276,7 @@ class OnScreenKeyboardWindow(ba.Window): on_activate_call=self._abc_mode) ba.buttonwidget(edit=self._emoji_button, color=self._key_color_dark, - label=charstr(SpCh.LOGO_FLAT), + label=ba.charstr(ba.SpecialChar.LOGO_FLAT), on_activate_call=self._next_mode) for i, btn in enumerate(self._char_keys): @@ -318,10 +319,11 @@ class OnScreenKeyboardWindow(ba.Window): def _next_keyboard(self) -> None: assert ba.app.meta.scanresults is not None - self._keyboard_index = (self._keyboard_index + 1) % len( - ba.app.meta.scanresults.keyboards) + kbexports = ba.app.meta.scanresults.exports_of_class(ba.Keyboard) + self._keyboard_index = (self._keyboard_index + 1) % len(kbexports) + self._load_keyboard() - if len(ba.app.meta.scanresults.keyboards) < 2: + if len(kbexports) < 2: ba.playsound(ba.getsound('error')) ba.screenmessage(ba.Lstr(resource='keyboardNoOthersAvailableText'), color=(1, 0, 0)) diff --git a/dist/ba_data/python/bastd/ui/party.py b/dist/ba_data/python/bastd/ui/party.py index 283b7a0..7de7b62 100644 --- a/dist/ba_data/python/bastd/ui/party.py +++ b/dist/ba_data/python/bastd/ui/party.py @@ -7,8 +7,8 @@ from __future__ import annotations import math from typing import TYPE_CHECKING, cast -import _ba import ba +import ba.internal from bastd.ui import popup if TYPE_CHECKING: @@ -19,10 +19,10 @@ class PartyWindow(ba.Window): """Party list/chat window.""" def __del__(self) -> None: - _ba.set_party_window_open(False) + ba.internal.set_party_window_open(False) def __init__(self, origin: Sequence[float] = (0, 0)): - _ba.set_party_window_open(True) + ba.internal.set_party_window_open(True) self._r = 'partyWindow' self._popup_type: str | None = None self._popup_party_member_client_id: int | None = None @@ -35,7 +35,7 @@ class PartyWindow(ba.Window): size=(self._width, self._height), transition='in_scale', color=(0.40, 0.55, 0.20), - parent=_ba.get_special_widget('overlay_stack'), + parent=ba.internal.get_special_widget('overlay_stack'), on_outside_click_call=self.close_with_sound, scale_origin_stack_offset=origin, scale=(2.0 if uiscale is ba.UIScale.SMALL else @@ -68,7 +68,7 @@ class PartyWindow(ba.Window): color=(0.55, 0.73, 0.25), iconscale=1.2) - info = _ba.get_connection_to_host_info() + info = ba.internal.get_connection_to_host_info() if info.get('name', '') != '': title = ba.Lstr(value=info['name']) else: @@ -116,7 +116,7 @@ class PartyWindow(ba.Window): # add all existing messages if chat is not muted if not ba.app.config.resolve('Chat Muted'): - msgs = _ba.get_chat_messages() + msgs = ba.internal.get_chat_messages() for msg in msgs: self._add_msg(msg) @@ -215,7 +215,7 @@ class PartyWindow(ba.Window): ba.textwidget(edit=self._muted_text, color=(1, 1, 1, 0.0)) # update roster section - roster = _ba.get_game_roster() + roster = ba.internal.get_game_roster() if roster != self._roster: self._roster = roster @@ -318,7 +318,7 @@ class PartyWindow(ba.Window): if is_host: twd = min( c_width * 0.85, - _ba.get_string_width( + ba.internal.get_string_width( p_str, suppress_warning=True) * t_scale) self._name_widgets.append( @@ -357,7 +357,7 @@ class PartyWindow(ba.Window): assert self._popup_party_member_client_id is not None # Ban for 5 minutes. - result = _ba.disconnect_client( + result = ba.internal.disconnect_client( self._popup_party_member_client_id, ban_time=5 * 60) if not result: ba.playsound(ba.getsound('error')) @@ -379,12 +379,12 @@ class PartyWindow(ba.Window): def _on_party_member_press(self, client_id: int, is_host: bool, widget: ba.Widget) -> None: # if we're the host, pop up 'kick' options for all non-host members - if _ba.get_foreground_host_session() is not None: + if ba.internal.get_foreground_host_session() is not None: kick_str = ba.Lstr(resource='kickText') else: # kick-votes appeared in build 14248 - if (_ba.get_connection_to_host_info().get('build_number', 0) < - 14248): + if (ba.internal.get_connection_to_host_info().get( + 'build_number', 0) < 14248): return kick_str = ba.Lstr(resource='kickVoteText') uiscale = ba.app.ui.uiscale @@ -401,7 +401,8 @@ class PartyWindow(ba.Window): self._popup_party_member_is_host = is_host def _send_chat_message(self) -> None: - _ba.chatmessage(cast(str, ba.textwidget(query=self._text_field))) + ba.internal.chatmessage( + cast(str, ba.textwidget(query=self._text_field))) ba.textwidget(edit=self._text_field, text='') def close(self) -> None: diff --git a/dist/ba_data/python/bastd/ui/partyqueue.py b/dist/ba_data/python/bastd/ui/partyqueue.py index c68878b..7867322 100644 --- a/dist/ba_data/python/bastd/ui/partyqueue.py +++ b/dist/ba_data/python/bastd/ui/partyqueue.py @@ -8,8 +8,8 @@ import random import time from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: from typing import Any, Sequence @@ -257,11 +257,11 @@ class PartyQueueWindow(ba.Window): def __del__(self) -> None: try: ba.app.ui.have_party_queue_window = False - _ba.add_transaction({ + ba.internal.add_transaction({ 'type': 'PARTY_QUEUE_REMOVE', 'q': self._queue_id }) - _ba.run_transactions() + ba.internal.run_transactions() except Exception: ba.print_exception('Error removing self from party queue.') @@ -320,8 +320,9 @@ class PartyQueueWindow(ba.Window): if -1 not in self._dudes_by_id: dude = self.Dude( self, response['d'], self._initial_offset, True, - _ba.get_v1_account_misc_read_val_2('resolvedAccountID', None), - _ba.get_v1_account_display_string()) + ba.internal.get_v1_account_misc_read_val_2( + 'resolvedAccountID', None), + ba.internal.get_v1_account_display_string()) self._dudes_by_id[-1] = dude self._dudes.append(dude) else: @@ -448,26 +449,26 @@ class PartyQueueWindow(ba.Window): now = time.time() if (self._last_connect_attempt_time is None or now - self._last_connect_attempt_time > 10.0): - _ba.connect_to_party(address=self._address, - port=self._port, - print_progress=False) + ba.internal.connect_to_party(address=self._address, + port=self._port, + print_progress=False) self._last_connect_attempt_time = now def on_boost_press(self) -> None: """Boost was pressed.""" from bastd.ui import account from bastd.ui import getcurrency - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() return - if _ba.get_v1_account_ticket_count() < self._boost_tickets: + if ba.internal.get_v1_account_ticket_count() < self._boost_tickets: ba.playsound(ba.getsound('error')) getcurrency.show_get_tickets_prompt() return ba.playsound(ba.getsound('laserReverse')) - _ba.add_transaction( + ba.internal.add_transaction( { 'type': 'PARTY_QUEUE_BOOST', 't': self._boost_tickets, @@ -497,18 +498,18 @@ class PartyQueueWindow(ba.Window): # Update boost button color based on if we have enough moola. if self._boost_button is not None: - can_boost = ( - (_ba.get_v1_account_state() == 'signed_in' - and _ba.get_v1_account_ticket_count() >= self._boost_tickets)) + can_boost = ((ba.internal.get_v1_account_state() == 'signed_in' + and ba.internal.get_v1_account_ticket_count() >= + self._boost_tickets)) ba.buttonwidget(edit=self._boost_button, color=(0, 1, 0) if can_boost else (0.7, 0.7, 0.7)) # Update ticket-count. if self._tickets_text is not None: if self._boost_button is not None: - if _ba.get_v1_account_state() == 'signed_in': + if ba.internal.get_v1_account_state() == 'signed_in': val = ba.charstr(ba.SpecialChar.TICKET) + str( - _ba.get_v1_account_ticket_count()) + ba.internal.get_v1_account_ticket_count()) else: val = ba.charstr(ba.SpecialChar.TICKET) + '???' ba.textwidget(edit=self._tickets_text, text=val) @@ -517,16 +518,16 @@ class PartyQueueWindow(ba.Window): current_time = ba.time(ba.TimeType.REAL) if (self._last_transaction_time is None - or current_time - self._last_transaction_time > - 0.001 * _ba.get_v1_account_misc_read_val('pqInt', 5000)): + or current_time - self._last_transaction_time > 0.001 * + ba.internal.get_v1_account_misc_read_val('pqInt', 5000)): self._last_transaction_time = current_time - _ba.add_transaction( + ba.internal.add_transaction( { 'type': 'PARTY_QUEUE_QUERY', 'q': self._queue_id }, callback=ba.WeakCall(self.on_update_response)) - _ba.run_transactions() + ba.internal.run_transactions() # step our dudes for dude in self._dudes: diff --git a/dist/ba_data/python/bastd/ui/play.py b/dist/ba_data/python/bastd/ui/play.py index 6f872f8..498c0cc 100644 --- a/dist/ba_data/python/bastd/ui/play.py +++ b/dist/ba_data/python/bastd/ui/play.py @@ -6,8 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: pass @@ -129,13 +129,15 @@ class PlayWindow(ba.Window): on_activate_call=self._coop) if ba.app.ui.use_toolbars and uiscale is ba.UIScale.SMALL: - ba.widget(edit=btn, - left_widget=_ba.get_special_widget('back_button')) - ba.widget(edit=btn, - up_widget=_ba.get_special_widget('account_button')) ba.widget( edit=btn, - down_widget=_ba.get_special_widget('settings_button')) + left_widget=ba.internal.get_special_widget('back_button')) + ba.widget( + edit=btn, + up_widget=ba.internal.get_special_widget('account_button')) + ba.widget(edit=btn, + down_widget=ba.internal.get_special_widget( + 'settings_button')) self._draw_dude(0, btn, @@ -216,9 +218,11 @@ class PlayWindow(ba.Window): on_activate_call=self._team_tourney) if ba.app.ui.use_toolbars: - ba.widget(edit=btn, - up_widget=_ba.get_special_widget('tickets_plus_button'), - right_widget=_ba.get_special_widget('party_button')) + ba.widget( + edit=btn, + up_widget=ba.internal.get_special_widget( + 'tickets_plus_button'), + right_widget=ba.internal.get_special_widget('party_button')) xxx = -14 self._draw_dude(2, @@ -447,7 +451,7 @@ class PlayWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui.account import show_sign_in_prompt from bastd.ui.coop.browser import CoopBrowserWindow - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return self._save_state() diff --git a/dist/ba_data/python/bastd/ui/playlist/addgame.py b/dist/ba_data/python/bastd/ui/playlist/addgame.py index 8ca5c6c..17af366 100644 --- a/dist/ba_data/python/bastd/ui/playlist/addgame.py +++ b/dist/ba_data/python/bastd/ui/playlist/addgame.py @@ -6,8 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: from bastd.ui.playlist.editcontroller import PlaylistEditController @@ -57,8 +57,9 @@ class PlaylistAddGameWindow(ba.Window): on_activate_call=self._add) if ba.app.ui.use_toolbars: - ba.widget(edit=select_button, - right_widget=_ba.get_special_widget('party_button')) + ba.widget( + edit=select_button, + right_widget=ba.internal.get_special_widget('party_button')) ba.textwidget(parent=self._root_widget, position=(self._width * 0.5, self._height - 28), @@ -116,9 +117,39 @@ class PlaylistAddGameWindow(ba.Window): ba.containerwidget(edit=self._root_widget, selected_child=self._scrollwidget) + self._game_types: list[type[ba.GameActivity]] = [] + + # Get actual games loading in the bg. + ba.app.meta.load_exported_classes(ba.GameActivity, + self._on_game_types_loaded, + completion_cb_in_bg_thread=True) + + # Refresh with our initial empty list. We'll refresh again once + # game loading is complete. self._refresh() + def _on_game_types_loaded(self, + gametypes: list[type[ba.GameActivity]]) -> None: + from ba.internal import get_unowned_game_types + + # We asked for a bg thread completion cb so we can do some + # filtering here in the bg thread where its not gonna cause hitches. + assert not ba.in_logic_thread() + sessiontype = self._editcontroller.get_session_type() + unowned = get_unowned_game_types() + self._game_types = [ + gt for gt in gametypes + if gt not in unowned and gt.supports_session_type(sessiontype) + ] + + # Sort in the current language. + self._game_types.sort(key=lambda g: g.get_display_string().evaluate()) + + # Tell ourself to refresh back in the logic thread. + ba.pushcall(self._refresh, from_other_thread=True) + def _refresh(self, select_get_more_games_button: bool = False) -> None: + # from ba.internal import get_game_types if self._column is not None: self._column.delete() @@ -127,15 +158,7 @@ class PlaylistAddGameWindow(ba.Window): border=2, margin=0) - gametypes = [ - gt for gt in ba.app.meta.get_game_types() if - gt.supports_session_type(self._editcontroller.get_session_type()) - ] - - # Sort in the current language. - gametypes.sort(key=lambda g: g.get_display_string().evaluate()) - - for i, gametype in enumerate(gametypes): + for i, gametype in enumerate(self._game_types): def _doit() -> None: if self._select_button: @@ -175,7 +198,7 @@ class PlaylistAddGameWindow(ba.Window): def _on_get_more_games_press(self) -> None: from bastd.ui.account import show_sign_in_prompt from bastd.ui.store.browser import StoreBrowserWindow - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return StoreBrowserWindow(modal=True, @@ -187,8 +210,8 @@ class PlaylistAddGameWindow(ba.Window): self._refresh(select_get_more_games_button=True) def _add(self) -> None: - _ba.lock_all_input() # Make sure no more commands happen. - ba.timer(0.1, _ba.unlock_all_input, timetype=ba.TimeType.REAL) + ba.internal.lock_all_input() # Make sure no more commands happen. + ba.timer(0.1, ba.internal.unlock_all_input, timetype=ba.TimeType.REAL) assert self._selected_game_type is not None self._editcontroller.add_game_type_selected(self._selected_game_type) diff --git a/dist/ba_data/python/bastd/ui/playlist/browser.py b/dist/ba_data/python/bastd/ui/playlist/browser.py index eace126..8a62126 100644 --- a/dist/ba_data/python/bastd/ui/playlist/browser.py +++ b/dist/ba_data/python/bastd/ui/playlist/browser.py @@ -8,8 +8,8 @@ import copy import math from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: pass @@ -140,8 +140,9 @@ class PlaylistBrowserWindow(ba.Window): def _ensure_standard_playlists_exist(self) -> None: # On new installations, go ahead and create a few playlists # besides the hard-coded default one: - if not _ba.get_v1_account_misc_val('madeStandardPlaylists', False): - _ba.add_transaction({ + if not ba.internal.get_v1_account_misc_val('madeStandardPlaylists', + False): + ba.internal.add_transaction({ 'type': 'ADD_PLAYLIST', 'playlistType': @@ -175,7 +176,7 @@ class PlaylistBrowserWindow(ba.Window): }, ] }) - _ba.add_transaction({ + ba.internal.add_transaction({ 'type': 'ADD_PLAYLIST', 'playlistType': @@ -226,7 +227,7 @@ class PlaylistBrowserWindow(ba.Window): }, ] }) - _ba.add_transaction({ + ba.internal.add_transaction({ 'type': 'ADD_PLAYLIST', 'playlistType': @@ -255,7 +256,7 @@ class PlaylistBrowserWindow(ba.Window): }, ] }) - _ba.add_transaction({ + ba.internal.add_transaction({ 'type': 'ADD_PLAYLIST', 'playlistType': @@ -274,12 +275,12 @@ class PlaylistBrowserWindow(ba.Window): } }] }) - _ba.add_transaction({ + ba.internal.add_transaction({ 'type': 'SET_MISC_VAL', 'name': 'madeStandardPlaylists', 'value': True }) - _ba.run_transactions() + ba.internal.run_transactions() def _refresh(self) -> None: # FIXME: Should tidy this up. @@ -367,14 +368,14 @@ class PlaylistBrowserWindow(ba.Window): if (x == 0 and ba.app.ui.use_toolbars and uiscale is ba.UIScale.SMALL): - ba.widget( - edit=btn, - left_widget=_ba.get_special_widget('back_button')) + ba.widget(edit=btn, + left_widget=ba.internal.get_special_widget( + 'back_button')) if (x == columns - 1 and ba.app.ui.use_toolbars and uiscale is ba.UIScale.SMALL): - ba.widget( - edit=btn, - right_widget=_ba.get_special_widget('party_button')) + ba.widget(edit=btn, + right_widget=ba.internal.get_special_widget( + 'party_button')) ba.buttonwidget( edit=btn, on_activate_call=ba.Call(self._on_playlist_press, btn, @@ -429,7 +430,8 @@ class PlaylistBrowserWindow(ba.Window): playlist = filter_playlist(playlist, self._sessiontype, remove_unowned=False, - mark_unowned=True) + mark_unowned=True, + name=name) for entry in playlist: mapname = entry['settings']['map'] maptype: type[ba.Map] | None diff --git a/dist/ba_data/python/bastd/ui/playlist/customizebrowser.py b/dist/ba_data/python/bastd/ui/playlist/customizebrowser.py index 6befcfd..2f99ad0 100644 --- a/dist/ba_data/python/bastd/ui/playlist/customizebrowser.py +++ b/dist/ba_data/python/bastd/ui/playlist/customizebrowser.py @@ -8,8 +8,8 @@ import copy import time from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: from typing import Any @@ -225,7 +225,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window): ba.widget(edit=btn, right_widget=scrollwidget) ba.widget(edit=scrollwidget, left_widget=new_button, - right_widget=_ba.get_special_widget('party_button') + right_widget=ba.internal.get_special_widget('party_button') if ba.app.ui.use_toolbars else None) # make sure config exists @@ -279,23 +279,23 @@ class PlaylistCustomizeBrowserWindow(ba.Window): def _run_selected_playlist(self) -> None: # pylint: disable=cyclic-import - _ba.unlock_all_input() + ba.internal.unlock_all_input() try: - _ba.new_host_session(self._sessiontype) + ba.internal.new_host_session(self._sessiontype) except Exception: from bastd import mainmenu ba.print_exception(f'Error running session {self._sessiontype}.') # Drop back into a main menu session. - _ba.new_host_session(mainmenu.MainMenuSession) + ba.internal.new_host_session(mainmenu.MainMenuSession) def _choose_playlist(self) -> None: if self._selected_playlist_name is None: return self._save_playlist_selection() ba.containerwidget(edit=self._root_widget, transition='out_left') - _ba.fade_screen(False, endcall=self._run_selected_playlist) - _ba.lock_all_input() + ba.internal.fade_screen(False, endcall=self._run_selected_playlist) + ba.internal.lock_all_input() def _refresh(self, select_playlist: str | None = None) -> None: from efro.util import asserttype @@ -424,12 +424,12 @@ class PlaylistCustomizeBrowserWindow(ba.Window): ba.containerwidget(edit=self._root_widget, transition='out_left') def _do_delete_playlist(self) -> None: - _ba.add_transaction({ + ba.internal.add_transaction({ 'type': 'REMOVE_PLAYLIST', 'playlistType': self._pvars.config_name, 'playlistName': self._selected_playlist_name }) - _ba.run_transactions() + ba.internal.run_transactions() ba.playsound(ba.getsound('shieldDown')) # (we don't use len()-1 here because the default list adds one) @@ -445,7 +445,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window): from bastd.ui.playlist import share # Gotta be signed in for this to work. - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': ba.screenmessage(ba.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0)) ba.playsound(ba.getsound('error')) @@ -477,7 +477,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window): return # Gotta be signed in for this to work. - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': ba.screenmessage(ba.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0)) ba.playsound(ba.getsound('error')) @@ -492,7 +492,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window): if self._selected_playlist_name is None: return - _ba.add_transaction( + ba.internal.add_transaction( { 'type': 'SHARE_PLAYLIST', 'expire_time': time.time() + 5, @@ -501,7 +501,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window): }, callback=ba.WeakCall(self._on_share_playlist_response, self._selected_playlist_name)) - _ba.run_transactions() + ba.internal.run_transactions() ba.screenmessage(ba.Lstr(resource='sharingText')) def _delete_playlist(self) -> None: @@ -582,13 +582,13 @@ class PlaylistCustomizeBrowserWindow(ba.Window): break test_index += 1 - _ba.add_transaction({ + ba.internal.add_transaction({ 'type': 'ADD_PLAYLIST', 'playlistType': self._pvars.config_name, 'playlistName': test_name, 'playlist': copy.deepcopy(plst) }) - _ba.run_transactions() + ba.internal.run_transactions() ba.playsound(ba.getsound('gunCocking')) self._refresh(select_playlist=test_name) diff --git a/dist/ba_data/python/bastd/ui/playlist/edit.py b/dist/ba_data/python/bastd/ui/playlist/edit.py index 42846d0..7e2040b 100644 --- a/dist/ba_data/python/bastd/ui/playlist/edit.py +++ b/dist/ba_data/python/bastd/ui/playlist/edit.py @@ -7,7 +7,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, cast import ba -import _ba +import ba.internal if TYPE_CHECKING: from bastd.ui.playlist.editcontroller import PlaylistEditController @@ -58,8 +58,9 @@ class PlaylistEditWindow(ba.Window): text_scale=1.2) if ba.app.ui.use_toolbars: - ba.widget(edit=btn, - right_widget=_ba.get_special_widget('party_button')) + ba.widget( + edit=btn, + right_widget=ba.internal.get_special_widget('party_button')) ba.widget(edit=cancel_button, left_widget=cancel_button, @@ -283,7 +284,7 @@ class PlaylistEditWindow(ba.Window): # If we had an old one, delete it. if self._editcontroller.get_existing_playlist_name() is not None: - _ba.add_transaction({ + ba.internal.add_transaction({ 'type': 'REMOVE_PLAYLIST', 'playlistType': @@ -292,13 +293,13 @@ class PlaylistEditWindow(ba.Window): self._editcontroller.get_existing_playlist_name() }) - _ba.add_transaction({ + ba.internal.add_transaction({ 'type': 'ADD_PLAYLIST', 'playlistType': self._editcontroller.get_config_name(), 'playlistName': new_name, 'playlist': self._editcontroller.get_playlist() }) - _ba.run_transactions() + ba.internal.run_transactions() ba.containerwidget(edit=self._root_widget, transition='out_right') ba.playsound(ba.getsound('gunCocking')) diff --git a/dist/ba_data/python/bastd/ui/playlist/editcontroller.py b/dist/ba_data/python/bastd/ui/playlist/editcontroller.py index c780135..664f649 100644 --- a/dist/ba_data/python/bastd/ui/playlist/editcontroller.py +++ b/dist/ba_data/python/bastd/ui/playlist/editcontroller.py @@ -52,7 +52,8 @@ class PlaylistEditController: appconfig[self._pvars.config_name + ' Playlists'][existing_playlist_name], sessiontype=sessiontype, - remove_unowned=False) + remove_unowned=False, + name=existing_playlist_name) self._edit_ui_selection = None else: if playlist is not None: diff --git a/dist/ba_data/python/bastd/ui/playlist/editgame.py b/dist/ba_data/python/bastd/ui/playlist/editgame.py index 9674357..ece7304 100644 --- a/dist/ba_data/python/bastd/ui/playlist/editgame.py +++ b/dist/ba_data/python/bastd/ui/playlist/editgame.py @@ -8,8 +8,8 @@ import copy import random from typing import TYPE_CHECKING, cast -import _ba import ba +import ba.internal if TYPE_CHECKING: from typing import Any, Callable @@ -134,7 +134,7 @@ class PlaylistEditGameWindow(ba.Window): resource='doneText')) if ba.app.ui.use_toolbars: - pbtn = _ba.get_special_widget('party_button') + pbtn = ba.internal.get_special_widget('party_button') ba.widget(edit=add_button, right_widget=pbtn, up_widget=pbtn) ba.textwidget(parent=self._root_widget, diff --git a/dist/ba_data/python/bastd/ui/playlist/mapselect.py b/dist/ba_data/python/bastd/ui/playlist/mapselect.py index eecbd1a..7e23d6a 100644 --- a/dist/ba_data/python/bastd/ui/playlist/mapselect.py +++ b/dist/ba_data/python/bastd/ui/playlist/mapselect.py @@ -7,8 +7,8 @@ from __future__ import annotations import math from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: from typing import Any, Callable @@ -165,9 +165,9 @@ class PlaylistMapSelectWindow(ba.Window): if y == 0: ba.widget(edit=btn, up_widget=self._cancel_button) if x == columns - 1 and ba.app.ui.use_toolbars: - ba.widget( - edit=btn, - right_widget=_ba.get_special_widget('party_button')) + ba.widget(edit=btn, + right_widget=ba.internal.get_special_widget( + 'party_button')) ba.widget(edit=btn, show_buffer_top=60, show_buffer_bottom=60) if self._maps[index][0] == self._previous_map: @@ -210,7 +210,7 @@ class PlaylistMapSelectWindow(ba.Window): def _on_store_press(self) -> None: from bastd.ui import account from bastd.ui.store.browser import StoreBrowserWindow - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() return StoreBrowserWindow(modal=True, @@ -236,8 +236,8 @@ class PlaylistMapSelectWindow(ba.Window): edit_info=self._edit_info).get_root_widget()) def _select_with_delay(self, map_name: str) -> None: - _ba.lock_all_input() - ba.timer(0.1, _ba.unlock_all_input, timetype=ba.TimeType.REAL) + ba.internal.lock_all_input() + ba.timer(0.1, ba.internal.unlock_all_input, timetype=ba.TimeType.REAL) ba.timer(0.1, ba.WeakCall(self._select, map_name), timetype=ba.TimeType.REAL) diff --git a/dist/ba_data/python/bastd/ui/playlist/share.py b/dist/ba_data/python/bastd/ui/playlist/share.py index 084fca6..9726545 100644 --- a/dist/ba_data/python/bastd/ui/playlist/share.py +++ b/dist/ba_data/python/bastd/ui/playlist/share.py @@ -7,8 +7,8 @@ from __future__ import annotations import time from typing import TYPE_CHECKING -import _ba import ba +import ba.internal from bastd.ui import promocode if TYPE_CHECKING: @@ -50,14 +50,14 @@ class SharePlaylistImportWindow(promocode.PromoCodeWindow): transition=self._transition_out) def _do_enter(self) -> None: - _ba.add_transaction( + ba.internal.add_transaction( { 'type': 'IMPORT_PLAYLIST', 'expire_time': time.time() + 5, 'code': ba.textwidget(query=self._text_field) }, callback=ba.WeakCall(self._on_import_response)) - _ba.run_transactions() + ba.internal.run_transactions() ba.screenmessage(ba.Lstr(resource='importingText')) diff --git a/dist/ba_data/python/bastd/ui/playoptions.py b/dist/ba_data/python/bastd/ui/playoptions.py index b200aec..8dfb4aa 100644 --- a/dist/ba_data/python/bastd/ui/playoptions.py +++ b/dist/ba_data/python/bastd/ui/playoptions.py @@ -6,8 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba +import ba.internal from bastd.ui import popup if TYPE_CHECKING: @@ -86,7 +86,8 @@ class PlayOptionsWindow(popup.PopupWindow): plst = filter_playlist(plst, self._sessiontype, remove_unowned=False, - mark_unowned=True) + mark_unowned=True, + name=name) game_count = len(plst) for entry in plst: mapname = entry['settings']['map'] @@ -349,7 +350,7 @@ class PlayOptionsWindow(popup.PopupWindow): from bastd.ui.teamnamescolors import TeamNamesColorsWindow from bastd.ui.purchase import PurchaseWindow if not ba.app.accounts_v1.have_pro(): - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': show_sign_in_prompt() else: PurchaseWindow(items=['pro']) @@ -417,8 +418,8 @@ class PlayOptionsWindow(popup.PopupWindow): if self._delegate is not None: self._delegate.on_play_options_window_run_game() else: - _ba.fade_screen(False, endcall=self._run_selected_playlist) - _ba.lock_all_input() + ba.internal.fade_screen(False, endcall=self._run_selected_playlist) + ba.internal.lock_all_input() self._transition_out(transition='out_left') if self._delegate is not None: self._delegate.on_play_options_window_run_game() @@ -426,12 +427,12 @@ class PlayOptionsWindow(popup.PopupWindow): cfg.commit() def _run_selected_playlist(self) -> None: - _ba.unlock_all_input() + ba.internal.unlock_all_input() try: - _ba.new_host_session(self._sessiontype) + ba.internal.new_host_session(self._sessiontype) except Exception: from bastd import mainmenu ba.print_exception('exception running session', self._sessiontype) # Drop back into a main menu session. - _ba.new_host_session(mainmenu.MainMenuSession) + ba.internal.new_host_session(mainmenu.MainMenuSession) diff --git a/dist/ba_data/python/bastd/ui/popup.py b/dist/ba_data/python/bastd/ui/popup.py index 0315b94..8f918fe 100644 --- a/dist/ba_data/python/bastd/ui/popup.py +++ b/dist/ba_data/python/bastd/ui/popup.py @@ -7,8 +7,8 @@ from __future__ import annotations import weakref from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: from typing import Any, Sequence, Callable @@ -81,7 +81,7 @@ class PopupWindow: scale=scale, toolbar_visibility=toolbar_visibility, size=size, - parent=_ba.get_special_widget('overlay_stack'), + parent=ba.internal.get_special_widget('overlay_stack'), stack_offset=(x_fin - x_offs, y_fin - y_offs), scale_origin_stack_offset=(position[0], position[1]), on_outside_click_call=self.on_popup_cancel, @@ -159,15 +159,15 @@ class PopupMenuWindow(PopupWindow): self._width, min( maxwidth, - _ba.get_string_width(choice_display_name, - suppress_warning=True)) + 75) + ba.internal.get_string_width( + choice_display_name, suppress_warning=True)) + 75) else: self._width = max( self._width, min( maxwidth, - _ba.get_string_width(choice_display_name, - suppress_warning=True)) + 60) + ba.internal.get_string_width( + choice_display_name, suppress_warning=True)) + 60) # init parent class - this will rescale and reposition things as # needed and create our root widget diff --git a/dist/ba_data/python/bastd/ui/profile/browser.py b/dist/ba_data/python/bastd/ui/profile/browser.py index 676b491..ff8c611 100644 --- a/dist/ba_data/python/bastd/ui/profile/browser.py +++ b/dist/ba_data/python/bastd/ui/profile/browser.py @@ -6,8 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: from typing import Any @@ -174,7 +174,8 @@ class ProfileBrowserWindow(ba.Window): from bastd.ui.purchase import PurchaseWindow # Limit to a handful profiles if they don't have pro-options. - max_non_pro_profiles = _ba.get_v1_account_misc_read_val('mnpp', 5) + max_non_pro_profiles = ba.internal.get_v1_account_misc_read_val( + 'mnpp', 5) assert self._profiles is not None if (not ba.app.accounts_v1.have_pro_options() and len(self._profiles) >= max_non_pro_profiles): @@ -221,11 +222,11 @@ class ProfileBrowserWindow(ba.Window): self._do_delete_profile, 350) def _do_delete_profile(self) -> None: - _ba.add_transaction({ + ba.internal.add_transaction({ 'type': 'REMOVE_PLAYER_PROFILE', 'name': self._selected_profile }) - _ba.run_transactions() + ba.internal.run_transactions() ba.playsound(ba.getsound('shieldDown')) self._refresh() @@ -283,8 +284,8 @@ class ProfileBrowserWindow(ba.Window): items.sort(key=lambda x: asserttype(x[0], str).lower()) index = 0 account_name: str | None - if _ba.get_v1_account_state() == 'signed_in': - account_name = _ba.get_v1_account_display_string() + if ba.internal.get_v1_account_state() == 'signed_in': + account_name = ba.internal.get_v1_account_display_string() else: account_name = None widget_to_select = None @@ -330,7 +331,7 @@ class ProfileBrowserWindow(ba.Window): # If there's a team-chooser in existence, tell it the profile-list # has probably changed. - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() if session is not None: session.handlemessage(PlayerProfilesChangedMessage()) diff --git a/dist/ba_data/python/bastd/ui/profile/edit.py b/dist/ba_data/python/bastd/ui/profile/edit.py index 64500c2..3304d1c 100644 --- a/dist/ba_data/python/bastd/ui/profile/edit.py +++ b/dist/ba_data/python/bastd/ui/profile/edit.py @@ -7,8 +7,8 @@ from __future__ import annotations import random from typing import TYPE_CHECKING, cast -import _ba import ba +import ba.internal if TYPE_CHECKING: from bastd.ui.colorpicker import ColorPicker @@ -144,7 +144,7 @@ class EditProfileWindow(ba.Window): # Assign a random name if they had none. if self._name == '': - names = _ba.get_random_names() + names = ba.internal.get_random_names() self._name = names[random.randrange(len(names))] self._clipped_name_text = ba.textwidget(parent=self._root_widget, @@ -172,8 +172,8 @@ class EditProfileWindow(ba.Window): self._upgrade_button = None if self._is_account_profile: - if _ba.get_v1_account_state() == 'signed_in': - sval = _ba.get_v1_account_display_string() + if ba.internal.get_v1_account_state() == 'signed_in': + sval = ba.internal.get_v1_account_display_string() else: sval = '??' ba.textwidget(parent=self._root_widget, @@ -188,7 +188,8 @@ class EditProfileWindow(ba.Window): resource='editProfileWindow.accountProfileText').evaluate() b_width = min( 270.0, - _ba.get_string_width(txtl, suppress_warning=True) * 0.6) + ba.internal.get_string_width(txtl, suppress_warning=True) * + 0.6) ba.textwidget(parent=self._root_widget, position=(self._width * 0.5, v - 39), size=(0, 0), @@ -258,7 +259,8 @@ class EditProfileWindow(ba.Window): resource='editProfileWindow.globalProfileText').evaluate() b_width = min( 240.0, - _ba.get_string_width(txtl, suppress_warning=True) * 0.6) + ba.internal.get_string_width(txtl, suppress_warning=True) * + 0.6) ba.textwidget(parent=self._root_widget, position=(self._width * 0.5, v - 39), size=(0, 0), @@ -299,7 +301,8 @@ class EditProfileWindow(ba.Window): resource='editProfileWindow.localProfileText').evaluate() b_width = min( 270.0, - _ba.get_string_width(txtl, suppress_warning=True) * 0.6) + ba.internal.get_string_width(txtl, suppress_warning=True) * + 0.6) ba.textwidget(parent=self._root_widget, position=(self._width * 0.5, v - 43), size=(0, 0), @@ -426,7 +429,7 @@ class EditProfileWindow(ba.Window): """Attempt to ugrade the profile to global.""" from bastd.ui import account from bastd.ui.profile import upgrade as pupgrade - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() return @@ -592,8 +595,9 @@ class EditProfileWindow(ba.Window): return name = self.getname() if name == '__account__': - name = (_ba.get_v1_account_name() - if _ba.get_v1_account_state() == 'signed_in' else '???') + name = (ba.internal.get_v1_account_name() + if ba.internal.get_v1_account_state() == 'signed_in' else + '???') if len(name) > 10 and not (self._global or self._is_account_profile): ba.textwidget(edit=self._clipped_name_text, text=ba.Lstr(resource='inGameClippedNameText', @@ -640,7 +644,7 @@ class EditProfileWindow(ba.Window): # Delete old in case we're renaming. if self._existing_profile and self._existing_profile != new_name: - _ba.add_transaction({ + ba.internal.add_transaction({ 'type': 'REMOVE_PLAYER_PROFILE', 'name': self._existing_profile }) @@ -649,7 +653,7 @@ class EditProfileWindow(ba.Window): # new name (will need to re-request it). self._global = False - _ba.add_transaction({ + ba.internal.add_transaction({ 'type': 'ADD_PLAYER_PROFILE', 'name': new_name, 'profile': { @@ -662,7 +666,7 @@ class EditProfileWindow(ba.Window): }) if transition_out: - _ba.run_transactions() + ba.internal.run_transactions() ba.containerwidget(edit=self._root_widget, transition='out_right') ba.app.ui.set_main_menu_window( ProfileBrowserWindow( diff --git a/dist/ba_data/python/bastd/ui/profile/upgrade.py b/dist/ba_data/python/bastd/ui/profile/upgrade.py index 3142dc9..ffce43e 100644 --- a/dist/ba_data/python/bastd/ui/profile/upgrade.py +++ b/dist/ba_data/python/bastd/ui/profile/upgrade.py @@ -8,8 +8,8 @@ import time import weakref from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: from typing import Any @@ -126,8 +126,8 @@ class ProfileUpgradeWindow(ba.Window): 'b': ba.app.build_number }, callback=ba.WeakCall(self._profile_check_result)) - self._cost = _ba.get_v1_account_misc_read_val('price.global_profile', - 500) + self._cost = ba.internal.get_v1_account_misc_read_val( + 'price.global_profile', 500) self._status: str | None = 'waiting' self._update_timer = ba.Timer(1.0, ba.WeakCall(self._update), @@ -170,7 +170,7 @@ class ProfileUpgradeWindow(ba.Window): from bastd.ui import getcurrency if self._status is None: # If it appears we don't have enough tickets, offer to buy more. - tickets = _ba.get_v1_account_ticket_count() + tickets = ba.internal.get_v1_account_ticket_count() if tickets < self._cost: ba.playsound(ba.getsound('error')) getcurrency.show_get_tickets_prompt() @@ -193,11 +193,11 @@ class ProfileUpgradeWindow(ba.Window): color=(1, 0, 0)) ba.playsound(ba.getsound('error')) return - _ba.add_transaction({ + ba.internal.add_transaction({ 'type': 'UPGRADE_PROFILE', 'name': self._name }) - _ba.run_transactions() + ba.internal.run_transactions() self._status = 'upgrading' self._upgrade_start_time = time.time() else: @@ -205,7 +205,7 @@ class ProfileUpgradeWindow(ba.Window): def _update(self) -> None: try: - t_str = str(_ba.get_v1_account_ticket_count()) + t_str = str(ba.internal.get_v1_account_ticket_count()) except Exception: t_str = '?' if self._tickets_text is not None: @@ -219,7 +219,7 @@ class ProfileUpgradeWindow(ba.Window): # Once we've kicked off an upgrade attempt and all transactions go # through, we're done. if (self._status == 'upgrading' - and not _ba.have_outstanding_transactions()): + and not ba.internal.have_outstanding_transactions()): self._status = 'exiting' ba.containerwidget(edit=self._root_widget, transition='out_right') edit_profile_window = self._edit_profile_window() diff --git a/dist/ba_data/python/bastd/ui/promocode.py b/dist/ba_data/python/bastd/ui/promocode.py index dc39078..78de497 100644 --- a/dist/ba_data/python/bastd/ui/promocode.py +++ b/dist/ba_data/python/bastd/ui/promocode.py @@ -7,8 +7,8 @@ from __future__ import annotations import time from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: pass @@ -112,9 +112,9 @@ class PromoCodeWindow(ba.Window): if not self._modal: ba.app.ui.set_main_menu_window( AdvancedSettingsWindow(transition='in_left').get_root_widget()) - _ba.add_transaction({ + ba.internal.add_transaction({ 'type': 'PROMO_CODE', 'expire_time': time.time() + 5, 'code': ba.textwidget(query=self._text_field) }) - _ba.run_transactions() + ba.internal.run_transactions() diff --git a/dist/ba_data/python/bastd/ui/purchase.py b/dist/ba_data/python/bastd/ui/purchase.py index 8b729c7..d28fa5a 100644 --- a/dist/ba_data/python/bastd/ui/purchase.py +++ b/dist/ba_data/python/bastd/ui/purchase.py @@ -6,8 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: from typing import Any @@ -68,11 +68,11 @@ class PurchaseWindow(ba.Window): pass # not working else: if self._items == ['pro']: - price_str = _ba.get_price(self._items[0]) + price_str = ba.internal.get_price(self._items[0]) pyoffs = -15 else: pyoffs = 0 - price = self._price = _ba.get_v1_account_misc_read_val( + price = self._price = ba.internal.get_v1_account_misc_read_val( 'price.' + str(items[0]), -1) price_str = ba.charstr(ba.SpecialChar.TICKET) + str(price) self._price_text = ba.textwidget(parent=self._root_widget, @@ -121,7 +121,7 @@ class PurchaseWindow(ba.Window): if ba.app.accounts_v1.have_pro(): can_die = True else: - if _ba.get_purchased(self._items[0]): + if ba.internal.get_purchased(self._items[0]): can_die = True if can_die: @@ -130,11 +130,11 @@ class PurchaseWindow(ba.Window): def _purchase(self) -> None: from bastd.ui import getcurrency if self._items == ['pro']: - _ba.purchase('pro') + ba.internal.purchase('pro') else: ticket_count: int | None try: - ticket_count = _ba.get_v1_account_ticket_count() + ticket_count = ba.internal.get_v1_account_ticket_count() except Exception: ticket_count = None if ticket_count is not None and ticket_count < self._price: @@ -143,7 +143,7 @@ class PurchaseWindow(ba.Window): return def do_it() -> None: - _ba.in_game_purchase(self._items[0], self._price) + ba.internal.in_game_purchase(self._items[0], self._price) ba.playsound(ba.getsound('swish')) do_it() diff --git a/dist/ba_data/python/bastd/ui/report.py b/dist/ba_data/python/bastd/ui/report.py index 1c76115..1f322ca 100644 --- a/dist/ba_data/python/bastd/ui/report.py +++ b/dist/ba_data/python/bastd/ui/report.py @@ -4,8 +4,8 @@ from __future__ import annotations -import _ba import ba +import ba.internal class ReportPlayerWindow(ba.Window): @@ -18,7 +18,7 @@ class ReportPlayerWindow(ba.Window): self._transition_out = 'out_scale' scale_origin = origin_widget.get_screen_space_center() - overlay_stack = _ba.get_special_widget('overlay_stack') + overlay_stack = ba.internal.get_special_widget('overlay_stack') uiscale = ba.app.ui.uiscale super().__init__(root_widget=ba.containerwidget( size=(self._width, self._height), @@ -63,27 +63,27 @@ class ReportPlayerWindow(ba.Window): def _on_language_press(self) -> None: from urllib import parse - _ba.add_transaction({ + ba.internal.add_transaction({ 'type': 'REPORT_ACCOUNT', 'reason': 'language', 'account': self._account_id }) body = ba.Lstr(resource='reportPlayerExplanationText').evaluate() ba.open_url('mailto:support@froemling.net' - f'?subject={_ba.appnameupper()} Player Report: ' + + f'?subject={ba.internal.appnameupper()} Player Report: ' + self._account_id + '&body=' + parse.quote(body)) self.close() def _on_cheating_press(self) -> None: from urllib import parse - _ba.add_transaction({ + ba.internal.add_transaction({ 'type': 'REPORT_ACCOUNT', 'reason': 'cheating', 'account': self._account_id }) body = ba.Lstr(resource='reportPlayerExplanationText').evaluate() ba.open_url('mailto:support@froemling.net' - f'?subject={_ba.appnameupper()} Player Report: ' + + f'?subject={ba.internal.appnameupper()} Player Report: ' + self._account_id + '&body=' + parse.quote(body)) self.close() diff --git a/dist/ba_data/python/bastd/ui/serverdialog.py b/dist/ba_data/python/bastd/ui/serverdialog.py index f972d06..8609982 100644 --- a/dist/ba_data/python/bastd/ui/serverdialog.py +++ b/dist/ba_data/python/bastd/ui/serverdialog.py @@ -4,35 +4,52 @@ from __future__ import annotations -from typing import TYPE_CHECKING +import logging +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Annotated + +from efro.dataclassio import ioprepped, IOAttrs -import _ba import ba +import ba.internal if TYPE_CHECKING: - from typing import Any + pass + + +@ioprepped +@dataclass +class ServerDialogData: + """Data for ServerDialog.""" + dialog_id: Annotated[str, IOAttrs('dialogID')] + text: Annotated[str, IOAttrs('text')] + subs: Annotated[list[tuple[str, str]], + IOAttrs('subs')] = field(default_factory=list) + show_cancel: Annotated[bool, IOAttrs('showCancel')] = True + copy_text: Annotated[str | None, IOAttrs('copyText')] = None class ServerDialogWindow(ba.Window): """A dialog window driven by the master-server.""" - def __init__(self, data: dict[str, Any]): - self._dialog_id = data['dialogID'] - txt = ba.Lstr(translate=('serverResponses', data['text']), - subs=data.get('subs', [])).evaluate() + def __init__(self, data: ServerDialogData): + self._data = data + txt = ba.Lstr(translate=('serverResponses', data.text), + subs=data.subs).evaluate() txt = txt.strip() txt_scale = 1.5 - txt_height = (_ba.get_string_height(txt, suppress_warning=True) * - txt_scale) + txt_height = ( + ba.internal.get_string_height(txt, suppress_warning=True) * + txt_scale) self._width = 500 - self._height = 130 + min(200, txt_height) + self._height = 160 + min(200, txt_height) uiscale = ba.app.ui.uiscale super().__init__(root_widget=ba.containerwidget( size=(self._width, self._height), transition='in_scale', scale=(1.8 if uiscale is ba.UIScale.SMALL else 1.35 if uiscale is ba.UIScale.MEDIUM else 1.0))) - self._starttime = ba.time(ba.TimeType.REAL, ba.TimeFormat.MILLISECONDS) + self._starttime = ba.time(ba.TimeType.REAL) ba.playsound(ba.getsound('swish')) ba.textwidget(parent=self._root_widget, @@ -46,51 +63,72 @@ class ServerDialogWindow(ba.Window): text=txt, maxwidth=self._width * 0.85, max_height=(self._height - 110)) - show_cancel = data.get('showCancel', True) - self._cancel_button: ba.Widget | None - if show_cancel: - self._cancel_button = ba.buttonwidget( - parent=self._root_widget, - position=(30, 30), - size=(160, 60), - autoselect=True, - label=ba.Lstr(resource='cancelText'), - on_activate_call=self._cancel_press) - else: - self._cancel_button = None + + show_copy = data.copy_text is not None and ba.clipboard_is_supported() + + # Currently can't do both copy and cancel since they go in the same + # spot. Cancel wins in this case since it is important functionality + # and copy is just for convenience (and not even always available). + if show_copy and data.show_cancel: + logging.warning('serverdialog requesting both copy and cancel;' + ' copy will not be shown.') + show_copy = False + + self._cancel_button = (None + if not data.show_cancel else ba.buttonwidget( + parent=self._root_widget, + position=(30, 30), + size=(160, 60), + autoselect=True, + label=ba.Lstr(resource='cancelText'), + on_activate_call=self._cancel_press)) + + self._copy_button = None if not show_copy else ba.buttonwidget( + parent=self._root_widget, + position=(30, 30), + size=(160, 60), + autoselect=True, + label=ba.Lstr(resource='copyText'), + on_activate_call=self._copy_press) + self._ok_button = ba.buttonwidget( parent=self._root_widget, - position=((self._width - 182) if show_cancel else + position=((self._width - 182) if + (data.show_cancel or show_copy) else (self._width * 0.5 - 80), 30), size=(160, 60), autoselect=True, label=ba.Lstr(resource='okText'), on_activate_call=self._ok_press) + ba.containerwidget(edit=self._root_widget, cancel_button=self._cancel_button, start_button=self._ok_button, selected_child=self._ok_button) + def _copy_press(self) -> None: + assert self._data.copy_text is not None + ba.clipboard_set_text(self._data.copy_text) + ba.screenmessage(ba.Lstr(resource='copyConfirmText'), color=(0, 1, 0)) + def _ok_press(self) -> None: - if ba.time(ba.TimeType.REAL, - ba.TimeFormat.MILLISECONDS) - self._starttime < 1000: + if ba.time(ba.TimeType.REAL) - self._starttime < 1.0: ba.playsound(ba.getsound('error')) return - _ba.add_transaction({ + ba.internal.add_transaction({ 'type': 'DIALOG_RESPONSE', - 'dialogID': self._dialog_id, + 'dialogID': self._data.dialog_id, 'response': 1 }) ba.containerwidget(edit=self._root_widget, transition='out_scale') def _cancel_press(self) -> None: - if ba.time(ba.TimeType.REAL, - ba.TimeFormat.MILLISECONDS) - self._starttime < 1000: + if ba.time(ba.TimeType.REAL) - self._starttime < 1.0: ba.playsound(ba.getsound('error')) return - _ba.add_transaction({ + ba.internal.add_transaction({ 'type': 'DIALOG_RESPONSE', - 'dialogID': self._dialog_id, + 'dialogID': self._data.dialog_id, 'response': 0 }) ba.containerwidget(edit=self._root_widget, transition='out_scale') diff --git a/dist/ba_data/python/bastd/ui/settings/advanced.py b/dist/ba_data/python/bastd/ui/settings/advanced.py index bed3f01..8a85f12 100644 --- a/dist/ba_data/python/bastd/ui/settings/advanced.py +++ b/dist/ba_data/python/bastd/ui/settings/advanced.py @@ -6,8 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba +import ba.internal from bastd.ui import popup as popup_ui if TYPE_CHECKING: @@ -200,10 +200,10 @@ class AdvancedSettingsWindow(ba.Window): # menu based on the language so still need this. ...however we could # make this more limited to it only rebuilds that one menu instead # of everything. - if self._menu_open or (self._prev_lang == _ba.app.config.get( + if self._menu_open or (self._prev_lang == ba.app.config.get( 'Lang', None) and self._prev_lang_list == available_languages): return - self._prev_lang = _ba.app.config.get('Lang', None) + self._prev_lang = ba.app.config.get('Lang', None) self._prev_lang_list = available_languages # Clear out our sub-container. @@ -251,8 +251,8 @@ class AdvancedSettingsWindow(ba.Window): h_align='right', v_align='center') - languages = _ba.app.lang.available_languages - cur_lang = _ba.app.config.get('Lang', None) + languages = ba.app.lang.available_languages + cur_lang = ba.app.config.get('Lang', None) if cur_lang is None: cur_lang = 'Auto' @@ -340,7 +340,7 @@ class AdvancedSettingsWindow(ba.Window): self._update_lang_status() v -= 40 - lang_inform = _ba.get_v1_account_misc_val('langInform', False) + lang_inform = ba.internal.get_v1_account_misc_val('langInform', False) self._language_inform_checkbox = cbw = ba.checkboxwidget( parent=self._subcontainer, @@ -432,7 +432,7 @@ class AdvancedSettingsWindow(ba.Window): label=ba.Lstr(resource=self._r + '.moddingGuideText'), text_scale=1.0, on_activate_call=ba.Call( - ba.open_url, 'http://ballistica.net/wiki/modding-guide')) + ba.open_url, 'https://ballistica.net/wiki/modding-guide')) if self._show_always_use_internal_keyboard: assert self._always_use_internal_keyboard_check_box is not None ba.widget(edit=self._always_use_internal_keyboard_check_box.widget, @@ -512,11 +512,12 @@ class AdvancedSettingsWindow(ba.Window): ba.widget(edit=child, show_buffer_bottom=30, show_buffer_top=20) if ba.app.ui.use_toolbars: - pbtn = _ba.get_special_widget('party_button') + pbtn = ba.internal.get_special_widget('party_button') ba.widget(edit=self._scrollwidget, right_widget=pbtn) if self._back_button is None: - ba.widget(edit=self._scrollwidget, - left_widget=_ba.get_special_widget('back_button')) + ba.widget( + edit=self._scrollwidget, + left_widget=ba.internal.get_special_widget('back_button')) self._restore_state() @@ -526,12 +527,12 @@ class AdvancedSettingsWindow(ba.Window): color=(1, 1, 0)) def _on_lang_inform_value_change(self, val: bool) -> None: - _ba.add_transaction({ + ba.internal.add_transaction({ 'type': 'SET_MISC_VAL', 'name': 'langInform', 'value': val }) - _ba.run_transactions() + ba.internal.run_transactions() def _on_vr_test_press(self) -> None: from bastd.ui.settings.vrtesting import VRTestingWindow @@ -544,7 +545,7 @@ class AdvancedSettingsWindow(ba.Window): from bastd.ui.settings.nettesting import NetTestingWindow # Net-testing requires a signed in v1 account. - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': ba.screenmessage(ba.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0)) ba.playsound(ba.getsound('error')) @@ -558,7 +559,7 @@ class AdvancedSettingsWindow(ba.Window): def _on_friend_promo_code_press(self) -> None: from bastd.ui import appinvite from bastd.ui import account - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() return appinvite.handle_app_invites_press() @@ -576,7 +577,7 @@ class AdvancedSettingsWindow(ba.Window): from bastd.ui.account import show_sign_in_prompt # We have to be logged in for promo-codes to work. - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return self._save_state() diff --git a/dist/ba_data/python/bastd/ui/settings/allsettings.py b/dist/ba_data/python/bastd/ui/settings/allsettings.py index bec6f05..b1f08f0 100644 --- a/dist/ba_data/python/bastd/ui/settings/allsettings.py +++ b/dist/ba_data/python/bastd/ui/settings/allsettings.py @@ -6,8 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: pass @@ -118,7 +118,7 @@ class AllSettingsWindow(ba.Window): label='', on_activate_call=self._do_controllers) if ba.app.ui.use_toolbars and self._back_button is None: - bbtn = _ba.get_special_widget('back_button') + bbtn = ba.internal.get_special_widget('back_button') ba.widget(edit=ctb, left_widget=bbtn) _b_title(x_offs2, v, ctb, ba.Lstr(resource=self._r + '.controllersText')) @@ -138,7 +138,7 @@ class AllSettingsWindow(ba.Window): label='', on_activate_call=self._do_graphics) if ba.app.ui.use_toolbars: - pbtn = _ba.get_special_widget('party_button') + pbtn = ba.internal.get_special_widget('party_button') ba.widget(edit=gfxb, up_widget=pbtn, right_widget=pbtn) _b_title(x_offs3, v, gfxb, ba.Lstr(resource=self._r + '.graphicsText')) imgw = imgh = 110 diff --git a/dist/ba_data/python/bastd/ui/settings/audio.py b/dist/ba_data/python/bastd/ui/settings/audio.py index bf93f6b..ad464c3 100644 --- a/dist/ba_data/python/bastd/ui/settings/audio.py +++ b/dist/ba_data/python/bastd/ui/settings/audio.py @@ -6,8 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: pass @@ -105,8 +105,9 @@ class AudioSettingsWindow(ba.Window): maxval=1.0, increment=0.1) if ba.app.ui.use_toolbars: - ba.widget(edit=svne.plusbutton, - right_widget=_ba.get_special_widget('party_button')) + ba.widget( + edit=svne.plusbutton, + right_widget=ba.internal.get_special_widget('party_button')) v -= spacing self._music_volume_numedit = ConfigNumberEdit( parent=self._root_widget, @@ -208,12 +209,13 @@ class AudioSettingsWindow(ba.Window): # We require disk access for soundtracks; # if we don't have it, request it. - if not _ba.have_permission(ba.Permission.STORAGE): + if not ba.internal.have_permission(ba.Permission.STORAGE): ba.playsound(ba.getsound('ding')) ba.screenmessage(ba.Lstr(resource='storagePermissionAccessText'), color=(0.5, 1, 0.5)) ba.timer(1.0, - ba.Call(_ba.request_permission, ba.Permission.STORAGE), + ba.Call(ba.internal.request_permission, + ba.Permission.STORAGE), timetype=ba.TimeType.REAL) return diff --git a/dist/ba_data/python/bastd/ui/settings/controls.py b/dist/ba_data/python/bastd/ui/settings/controls.py index 017568d..3e9566a 100644 --- a/dist/ba_data/python/bastd/ui/settings/controls.py +++ b/dist/ba_data/python/bastd/ui/settings/controls.py @@ -6,8 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: pass @@ -41,7 +41,7 @@ class ControlsSettingsWindow(ba.Window): self._r = 'configControllersWindow' app = ba.app - # is_fire_tv = _ba.is_running_on_fire_tv() + # is_fire_tv = ba.internal.is_running_on_fire_tv() spacing = 50.0 button_width = 350.0 @@ -63,7 +63,7 @@ class ControlsSettingsWindow(ba.Window): height += spacing show_touch = False - if _ba.have_touchscreen_input(): + if ba.internal.have_touchscreen_input(): show_touch = True height += spacing @@ -73,7 +73,8 @@ class ControlsSettingsWindow(ba.Window): height += space_height show_keyboard = False - if _ba.getinputdevice('Keyboard', '#1', doraise=False) is not None: + if ba.internal.getinputdevice('Keyboard', '#1', + doraise=False) is not None: show_keyboard = True height += spacing show_keyboard_p2 = False if app.vr_mode else show_keyboard @@ -101,7 +102,7 @@ class ControlsSettingsWindow(ba.Window): # (we can run into problems where devices register as one of each # type otherwise).. show_mac_controller_subsystem = False - if platform == 'mac' and _ba.is_xcode_build(): + if platform == 'mac' and ba.internal.is_xcode_build(): show_mac_controller_subsystem = True if show_mac_controller_subsystem: @@ -163,7 +164,8 @@ class ControlsSettingsWindow(ba.Window): on_activate_call=self._do_touchscreen) if ba.app.ui.use_toolbars: ba.widget(edit=btn, - right_widget=_ba.get_special_widget('party_button')) + right_widget=ba.internal.get_special_widget( + 'party_button')) if not self._have_selected_child: ba.containerwidget(edit=self._root_widget, selected_child=self._touch_button) @@ -182,7 +184,8 @@ class ControlsSettingsWindow(ba.Window): on_activate_call=self._do_gamepads) if ba.app.ui.use_toolbars: ba.widget(edit=btn, - right_widget=_ba.get_special_widget('party_button')) + right_widget=ba.internal.get_special_widget( + 'party_button')) if not self._have_selected_child: ba.containerwidget(edit=self._root_widget, selected_child=self._gamepads_button) @@ -206,7 +209,8 @@ class ControlsSettingsWindow(ba.Window): on_activate_call=self._config_keyboard) if ba.app.ui.use_toolbars: ba.widget(edit=btn, - right_widget=_ba.get_special_widget('party_button')) + right_widget=ba.internal.get_special_widget( + 'party_button')) if not self._have_selected_child: ba.containerwidget(edit=self._root_widget, selected_child=self._keyboard_button) @@ -235,7 +239,8 @@ class ControlsSettingsWindow(ba.Window): on_activate_call=self._do_mobile_devices) if ba.app.ui.use_toolbars: ba.widget(edit=btn, - right_widget=_ba.get_special_widget('party_button')) + right_widget=ba.internal.get_special_widget( + 'party_button')) if not self._have_selected_child: ba.containerwidget(edit=self._root_widget, selected_child=self._idevices_button) @@ -251,13 +256,15 @@ class ControlsSettingsWindow(ba.Window): ba.Lstr(resource='settingsWindowAdvanced.mustRestartText'), color=(1, 1, 0)) ba.playsound(ba.getsound('gunCocking')) - _ba.set_low_level_config_value('enablexinput', not value) + ba.internal.set_low_level_config_value('enablexinput', + not value) ba.checkboxwidget( parent=self._root_widget, position=(100, v + 3), size=(120, 30), - value=(not _ba.get_low_level_config_value('enablexinput', 1)), + value=(not ba.internal.get_low_level_config_value( + 'enablexinput', 1)), maxwidth=200, on_value_change_call=do_toggle, text=ba.Lstr(resource='disableXInputText'), @@ -323,8 +330,8 @@ class ControlsSettingsWindow(ba.Window): self._save_state() ba.containerwidget(edit=self._root_widget, transition='out_left') ba.app.ui.set_main_menu_window( - ConfigKeyboardWindow(_ba.getinputdevice('Keyboard', - '#1')).get_root_widget()) + ConfigKeyboardWindow(ba.internal.getinputdevice( + 'Keyboard', '#1')).get_root_widget()) def _config_keyboard2(self) -> None: # pylint: disable=cyclic-import @@ -332,8 +339,8 @@ class ControlsSettingsWindow(ba.Window): self._save_state() ba.containerwidget(edit=self._root_widget, transition='out_left') ba.app.ui.set_main_menu_window( - ConfigKeyboardWindow(_ba.getinputdevice('Keyboard', - '#2')).get_root_widget()) + ConfigKeyboardWindow(ba.internal.getinputdevice( + 'Keyboard', '#2')).get_root_widget()) def _do_mobile_devices(self) -> None: # pylint: disable=cyclic-import diff --git a/dist/ba_data/python/bastd/ui/settings/gamepad.py b/dist/ba_data/python/bastd/ui/settings/gamepad.py index ea86a95..4d5d946 100644 --- a/dist/ba_data/python/bastd/ui/settings/gamepad.py +++ b/dist/ba_data/python/bastd/ui/settings/gamepad.py @@ -6,8 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: from typing import Any, Callable @@ -801,7 +801,7 @@ class AwaitGamepadInputWindow(ba.Window): ba.Call(self._decrement), repeat=True, timetype=ba.TimeType.REAL) - _ba.capture_gamepad_input(ba.WeakCall(self._event_callback)) + ba.internal.capture_gamepad_input(ba.WeakCall(self._event_callback)) def __del__(self) -> None: pass @@ -811,7 +811,7 @@ class AwaitGamepadInputWindow(ba.Window): # This strong-refs us; killing it allow us to die now. self._decrement_timer = None - _ba.release_gamepad_input() + ba.internal.release_gamepad_input() if self._root_widget: ba.containerwidget(edit=self._root_widget, transition='out_scale') diff --git a/dist/ba_data/python/bastd/ui/settings/gamepadselect.py b/dist/ba_data/python/bastd/ui/settings/gamepadselect.py index 8053fe3..65bdf26 100644 --- a/dist/ba_data/python/bastd/ui/settings/gamepadselect.py +++ b/dist/ba_data/python/bastd/ui/settings/gamepadselect.py @@ -6,8 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: from typing import Any @@ -21,7 +21,7 @@ def gamepad_configure_callback(event: dict[str, Any]) -> None: # Ignore all but button-presses. if event['type'] not in ['BUTTONDOWN', 'HATMOTION']: return - _ba.release_gamepad_input() + ba.internal.release_gamepad_input() try: ba.app.ui.clear_main_menu_window(transition='out_left') except Exception: @@ -140,11 +140,11 @@ class GamepadSelectWindow(ba.Window): h_align='center', v_align='top') - _ba.capture_gamepad_input(gamepad_configure_callback) + ba.internal.capture_gamepad_input(gamepad_configure_callback) def _back(self) -> None: from bastd.ui.settings import controls - _ba.release_gamepad_input() + ba.internal.release_gamepad_input() ba.containerwidget(edit=self._root_widget, transition='out_right') ba.app.ui.set_main_menu_window( controls.ControlsSettingsWindow( diff --git a/dist/ba_data/python/bastd/ui/settings/graphics.py b/dist/ba_data/python/bastd/ui/settings/graphics.py index 8d0fff0..09eb1f2 100644 --- a/dist/ba_data/python/bastd/ui/settings/graphics.py +++ b/dist/ba_data/python/bastd/ui/settings/graphics.py @@ -6,8 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: pass @@ -52,7 +52,7 @@ class GraphicsSettingsWindow(ba.Window): show_gamma = False gamma_spacing = spacing * 1.3 - if _ba.has_gamma_control(): + if ba.internal.has_gamma_control(): show_gamma = True height += gamma_spacing @@ -137,7 +137,8 @@ class GraphicsSettingsWindow(ba.Window): textscale=0.85) if ba.app.ui.use_toolbars: ba.widget(edit=gmc.plusbutton, - right_widget=_ba.get_special_widget('party_button')) + right_widget=ba.internal.get_special_widget( + 'party_button')) if not self._have_selected_child: ba.containerwidget(edit=self._root_widget, selected_child=gmc.minusbutton) @@ -166,7 +167,7 @@ class GraphicsSettingsWindow(ba.Window): scale=popup_menu_scale, choices=['Auto', 'Higher', 'High', 'Medium', 'Low'], choices_disabled=['Higher', 'High'] - if _ba.get_max_graphics_quality() == 'Medium' else [], + if ba.internal.get_max_graphics_quality() == 'Medium' else [], choices_display=[ ba.Lstr(resource='autoText'), ba.Lstr(resource=self._r + '.higherText'), @@ -202,8 +203,9 @@ class GraphicsSettingsWindow(ba.Window): current_choice=ba.app.config.resolve('Texture Quality'), on_value_change_call=self._set_textures) if ba.app.ui.use_toolbars: - ba.widget(edit=textures_popup.get_button(), - right_widget=_ba.get_special_widget('party_button')) + ba.widget( + edit=textures_popup.get_button(), + right_widget=ba.internal.get_special_widget('party_button')) v -= 80 h_offs = 0 @@ -238,7 +240,7 @@ class GraphicsSettingsWindow(ba.Window): current_choice=current_res_cardboard, on_value_change_call=self._set_gvr_render_target_scale) else: - native_res = _ba.get_display_resolution() + native_res = ba.internal.get_display_resolution() assert native_res is not None choices = ['Auto', 'Native'] choices_display = [ @@ -265,7 +267,7 @@ class GraphicsSettingsWindow(ba.Window): else: # if we're on a system that doesn't allow setting resolution, # set pixel-scale instead - current_res = _ba.get_display_resolution() + current_res = ba.internal.get_display_resolution() if current_res is None: current_res2 = (str(min(100, max(10, int(round( ba.app.config.resolve('Screen Pixel Scale') diff --git a/dist/ba_data/python/bastd/ui/settings/keyboard.py b/dist/ba_data/python/bastd/ui/settings/keyboard.py index 2e93f1f..4a2d056 100644 --- a/dist/ba_data/python/bastd/ui/settings/keyboard.py +++ b/dist/ba_data/python/bastd/ui/settings/keyboard.py @@ -6,8 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: from typing import Any @@ -281,10 +281,10 @@ class AwaitKeyboardInputWindow(ba.Window): text=str(self._counter)) self._decrement_timer: ba.Timer | None = ba.Timer( 1.0, self._decrement, repeat=True, timetype=ba.TimeType.REAL) - _ba.capture_keyboard_input(ba.WeakCall(self._button_callback)) + ba.internal.capture_keyboard_input(ba.WeakCall(self._button_callback)) def __del__(self) -> None: - _ba.release_keyboard_input() + ba.internal.release_keyboard_input() def _die(self) -> None: # This strong-refs us; killing it allows us to die now. diff --git a/dist/ba_data/python/bastd/ui/settings/nettesting.py b/dist/ba_data/python/bastd/ui/settings/nettesting.py index 9c2700f..9ea4c9b 100644 --- a/dist/ba_data/python/bastd/ui/settings/nettesting.py +++ b/dist/ba_data/python/bastd/ui/settings/nettesting.py @@ -11,13 +11,17 @@ from threading import Thread from typing import TYPE_CHECKING from efro.error import CleanError -import _ba import ba +import ba.internal from bastd.ui.settings.testing import TestingWindow if TYPE_CHECKING: from typing import Callable, Any +# We generally want all net tests to timeout on their own, but we add +# sort of sane max in case they don't. +MAX_TEST_SECONDS = 60 * 2 + class NetTestingWindow(ba.Window): """Window that runs a networking test suite to help diagnose issues.""" @@ -160,7 +164,7 @@ def _run_diagnostics(weakwin: weakref.ref[NetTestingWindow]) -> None: try: _print(f'Running network diagnostics...\n' - f'ua: {_ba.app.user_agent_string}\n' + f'ua: {ba.app.user_agent_string}\n' f'time: {utc_now()}.') if bool(False): @@ -171,12 +175,12 @@ def _run_diagnostics(weakwin: weakref.ref[NetTestingWindow]) -> None: _print_test_results(_dummy_fail) # V1 ping - baseaddr = _ba.get_master_server_address(source=0, version=1) + baseaddr = ba.internal.get_master_server_address(source=0, version=1) _print(f'\nContacting V1 master-server src0 ({baseaddr})...') _print_test_results(lambda: _test_fetch(baseaddr)) # V1 alternate ping - baseaddr = _ba.get_master_server_address(source=1, version=1) + baseaddr = ba.internal.get_master_server_address(source=1, version=1) _print(f'\nContacting V1 master-server src1 ({baseaddr})...') _print_test_results(lambda: _test_fetch(baseaddr)) @@ -185,14 +189,14 @@ def _run_diagnostics(weakwin: weakref.ref[NetTestingWindow]) -> None: for srcid, result in sorted(ba.app.net.v1_ctest_results.items()): _print(f'\nV1 src{srcid} result: {result}') - curv1addr = _ba.get_master_server_address(version=1) + curv1addr = ba.internal.get_master_server_address(version=1) _print(f'\nUsing V1 address: {curv1addr}') _print('\nRunning V1 transaction...') _print_test_results(_test_v1_transaction) # V2 ping - baseaddr = _ba.get_master_server_address(version=2) + baseaddr = ba.internal.get_master_server_address(version=2) _print(f'\nContacting V2 master-server ({baseaddr})...') _print_test_results(lambda: _test_fetch(baseaddr)) @@ -242,7 +246,7 @@ def _dummy_fail() -> None: def _test_v1_transaction() -> None: """Dummy fail test case.""" - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': raise RuntimeError('Not signed in.') starttime = time.monotonic() @@ -259,21 +263,22 @@ def _test_v1_transaction() -> None: def _do_it() -> None: # Fire off a transaction with a callback. - _ba.add_transaction( + ba.internal.add_transaction( { 'type': 'PRIVATE_PARTY_QUERY', 'expire_time': time.time() + 20, }, callback=_cb, ) - _ba.run_transactions() + ba.internal.run_transactions() ba.pushcall(_do_it, from_other_thread=True) while results[0] is False: time.sleep(0.01) - if time.monotonic() - starttime > 10.0: - raise RuntimeError('timed out') + if time.monotonic() - starttime > MAX_TEST_SECONDS: + raise RuntimeError( + f'test timed out after {MAX_TEST_SECONDS} seconds') # If we got left a string, its an error. if isinstance(results[0], str): @@ -313,8 +318,9 @@ def _test_v2_cloud_message() -> None: if results.response_time is not None: break time.sleep(0.01) - if time.monotonic() - wait_start_time > 10.0: - raise RuntimeError('Timeout waiting for cloud message response') + if time.monotonic() - wait_start_time > MAX_TEST_SECONDS: + raise RuntimeError(f'Timeout ({MAX_TEST_SECONDS} seconds)' + f' waiting for cloud message response') if results.errstr is not None: raise RuntimeError(results.errstr) @@ -337,9 +343,9 @@ def _test_fetch(baseaddr: str) -> None: import urllib.request response = urllib.request.urlopen( urllib.request.Request(f'{baseaddr}/ping', None, - {'User-Agent': _ba.app.user_agent_string}), + {'User-Agent': ba.app.user_agent_string}), context=ba.app.net.sslcontext, - timeout=10.0, + timeout=MAX_TEST_SECONDS, ) if response.getcode() != 200: raise RuntimeError( diff --git a/dist/ba_data/python/bastd/ui/settings/testing.py b/dist/ba_data/python/bastd/ui/settings/testing.py index d1f7fb6..53b63af 100644 --- a/dist/ba_data/python/bastd/ui/settings/testing.py +++ b/dist/ba_data/python/bastd/ui/settings/testing.py @@ -7,8 +7,8 @@ from __future__ import annotations import copy from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: from typing import Any, Callable @@ -96,7 +96,7 @@ class TestingWindow(ba.Window): # we can reset if we want.. if entry_name not in ba.app.value_test_defaults: ba.app.value_test_defaults[entry_name] = ( - _ba.value_test(entry_name)) + ba.internal.value_test(entry_name)) ba.textwidget(parent=self._subcontainer, position=(h, v), @@ -125,7 +125,7 @@ class TestingWindow(ba.Window): v_align='center', maxwidth=60, text='%.4g' % - _ba.value_test(entry_name)) + ba.internal.value_test(entry_name)) btn = ba.buttonwidget(parent=self._subcontainer, position=(h + 140, v - 19), size=(40, 40), @@ -156,25 +156,26 @@ class TestingWindow(ba.Window): def _on_reset_press(self) -> None: for entry in self._entries: - _ba.value_test(entry['name'], - absolute=ba.app.value_test_defaults[entry['name']]) + ba.internal.value_test( + entry['name'], + absolute=ba.app.value_test_defaults[entry['name']]) # pylint: disable=consider-using-f-string ba.textwidget(edit=entry['widget'], - text='%.4g' % _ba.value_test(entry['name'])) + text='%.4g' % ba.internal.value_test(entry['name'])) def _on_minus_press(self, entry_name: str) -> None: entry = self._get_entry(entry_name) - _ba.value_test(entry['name'], change=-entry['increment']) + ba.internal.value_test(entry['name'], change=-entry['increment']) # pylint: disable=consider-using-f-string ba.textwidget(edit=entry['widget'], - text='%.4g' % _ba.value_test(entry['name'])) + text='%.4g' % ba.internal.value_test(entry['name'])) def _on_plus_press(self, entry_name: str) -> None: entry = self._get_entry(entry_name) - _ba.value_test(entry['name'], change=entry['increment']) + ba.internal.value_test(entry['name'], change=entry['increment']) # pylint: disable=consider-using-f-string ba.textwidget(edit=entry['widget'], - text='%.4g' % _ba.value_test(entry['name'])) + text='%.4g' % ba.internal.value_test(entry['name'])) def _do_back(self) -> None: # pylint: disable=cyclic-import diff --git a/dist/ba_data/python/bastd/ui/settings/touchscreen.py b/dist/ba_data/python/bastd/ui/settings/touchscreen.py index ddf4346..f8d7979 100644 --- a/dist/ba_data/python/bastd/ui/settings/touchscreen.py +++ b/dist/ba_data/python/bastd/ui/settings/touchscreen.py @@ -3,8 +3,8 @@ """UI settings functionality related to touchscreens.""" from __future__ import annotations -import _ba import ba +import ba.internal class TouchscreenSettingsWindow(ba.Window): @@ -16,7 +16,7 @@ class TouchscreenSettingsWindow(ba.Window): # FIXME: Could switch to a UI destroy callback now that those are a # thing that exists. - _ba.set_touchscreen_editing(False) + ba.internal.set_touchscreen_editing(False) def __init__(self) -> None: @@ -25,7 +25,7 @@ class TouchscreenSettingsWindow(ba.Window): self._spacing = 40 self._r = 'configTouchscreenWindow' - _ba.set_touchscreen_editing(True) + ba.internal.set_touchscreen_editing(True) uiscale = ba.app.ui.uiscale super().__init__(root_widget=ba.containerwidget( @@ -233,4 +233,4 @@ class TouchscreenSettingsWindow(ba.Window): ba.app.ui.set_main_menu_window( controls.ControlsSettingsWindow( transition='in_left').get_root_widget()) - _ba.set_touchscreen_editing(False) + ba.internal.set_touchscreen_editing(False) diff --git a/dist/ba_data/python/bastd/ui/settings/xbox360controller.py b/dist/ba_data/python/bastd/ui/settings/xbox360controller.py index 0e145a7..bdec91d 100644 --- a/dist/ba_data/python/bastd/ui/settings/xbox360controller.py +++ b/dist/ba_data/python/bastd/ui/settings/xbox360controller.py @@ -6,8 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: pass @@ -19,7 +19,7 @@ class XBox360ControllerSettingsWindow(ba.Window): def __init__(self) -> None: self._r = 'xbox360ControllersWindow' width = 700 - height = 300 if _ba.is_running_on_fire_tv() else 485 + height = 300 if ba.internal.is_running_on_fire_tv() else 485 spacing = 40 uiscale = ba.app.ui.uiscale super().__init__(root_widget=ba.containerwidget( @@ -58,7 +58,7 @@ class XBox360ControllerSettingsWindow(ba.Window): v = height - 70 v -= spacing - if _ba.is_running_on_fire_tv(): + if ba.internal.is_running_on_fire_tv(): ba.textwidget(parent=self._root_widget, position=(width * 0.5, height * 0.47), size=(0, 0), diff --git a/dist/ba_data/python/bastd/ui/soundtrack/browser.py b/dist/ba_data/python/bastd/ui/soundtrack/browser.py index b208815..a4bf0dd 100644 --- a/dist/ba_data/python/bastd/ui/soundtrack/browser.py +++ b/dist/ba_data/python/bastd/ui/soundtrack/browser.py @@ -7,8 +7,8 @@ from __future__ import annotations import copy from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: from typing import Any @@ -105,8 +105,9 @@ class SoundtrackBrowserWindow(ba.Window): texture=lock_tex)) if self._back_button is None: - ba.widget(edit=btn, - left_widget=_ba.get_special_widget('back_button')) + ba.widget( + edit=btn, + left_widget=ba.internal.get_special_widget('back_button')) v -= 60.0 * scl self._edit_button = btn = ba.buttonwidget( @@ -127,8 +128,9 @@ class SoundtrackBrowserWindow(ba.Window): position=(h - 10, v + 55.0 * scl - 28), texture=lock_tex)) if self._back_button is None: - ba.widget(edit=btn, - left_widget=_ba.get_special_widget('back_button')) + ba.widget( + edit=btn, + left_widget=ba.internal.get_special_widget('back_button')) v -= 60.0 * scl self._duplicate_button = btn = ba.buttonwidget( @@ -149,8 +151,9 @@ class SoundtrackBrowserWindow(ba.Window): position=(h - 10, v + 55.0 * scl - 28), texture=lock_tex)) if self._back_button is None: - ba.widget(edit=btn, - left_widget=_ba.get_special_widget('back_button')) + ba.widget( + edit=btn, + left_widget=ba.internal.get_special_widget('back_button')) v -= 60.0 * scl self._delete_button = btn = ba.buttonwidget( @@ -171,8 +174,9 @@ class SoundtrackBrowserWindow(ba.Window): position=(h - 10, v + 55.0 * scl - 28), texture=lock_tex)) if self._back_button is None: - ba.widget(edit=btn, - left_widget=_ba.get_special_widget('back_button')) + ba.widget( + edit=btn, + left_widget=ba.internal.get_special_widget('back_button')) # Keep our lock images up to date/etc. self._update_timer = ba.Timer(1.0, @@ -191,7 +195,7 @@ class SoundtrackBrowserWindow(ba.Window): size=(self._width - (205 + 2 * x_inset), scroll_height)) ba.widget(edit=self._scrollwidget, left_widget=self._new_button, - right_widget=_ba.get_special_widget('party_button') + right_widget=ba.internal.get_special_widget('party_button') if ba.app.ui.use_toolbars else self._scrollwidget) self._col = ba.columnwidget(parent=scrollwidget, border=2, margin=0) diff --git a/dist/ba_data/python/bastd/ui/soundtrack/entrytypeselect.py b/dist/ba_data/python/bastd/ui/soundtrack/entrytypeselect.py index acfdef2..e009b25 100644 --- a/dist/ba_data/python/bastd/ui/soundtrack/entrytypeselect.py +++ b/dist/ba_data/python/bastd/ui/soundtrack/entrytypeselect.py @@ -6,8 +6,8 @@ from __future__ import annotations import copy from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: from typing import Any, Callable @@ -160,7 +160,7 @@ class SoundtrackEntryTypeSelectWindow(ba.Window): from ba.osmusic import OSMusicPlayer from bastd.ui.fileselector import FileSelectorWindow ba.containerwidget(edit=self._root_widget, transition='out_left') - base_path = _ba.android_get_external_files_dir() + base_path = ba.internal.android_get_external_files_dir() ba.app.ui.set_main_menu_window( FileSelectorWindow( base_path, @@ -173,7 +173,7 @@ class SoundtrackEntryTypeSelectWindow(ba.Window): def _on_music_folder_press(self) -> None: from bastd.ui.fileselector import FileSelectorWindow ba.containerwidget(edit=self._root_widget, transition='out_left') - base_path = _ba.android_get_external_files_dir() + base_path = ba.internal.android_get_external_files_dir() ba.app.ui.set_main_menu_window( FileSelectorWindow(base_path, callback=self._music_folder_selector_cb, diff --git a/dist/ba_data/python/bastd/ui/specialoffer.py b/dist/ba_data/python/bastd/ui/specialoffer.py index 82a20d4..1935f3b 100644 --- a/dist/ba_data/python/bastd/ui/specialoffer.py +++ b/dist/ba_data/python/bastd/ui/specialoffer.py @@ -7,8 +7,8 @@ from __future__ import annotations import copy from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: from typing import Any @@ -35,15 +35,15 @@ class SpecialOfferWindow(ba.Window): # Misnomer: 'pro' actually means offer 'pro_sale'. if offer['item'] in ['pro', 'pro_fullprice']: - real_price = _ba.get_price('pro' if offer['item'] == - 'pro_fullprice' else 'pro_sale') + real_price = ba.internal.get_price('pro' if offer['item'] == + 'pro_fullprice' else 'pro_sale') if real_price is None and ba.app.debug_build: print('NOTE: Faking prices for debug build.') real_price = '$1.23' zombie = real_price is None elif isinstance(offer['price'], str): # (a string price implies IAP id) - real_price = _ba.get_price(offer['price']) + real_price = ba.internal.get_price(offer['price']) if real_price is None and ba.app.debug_build: print('NOTE: Faking price for debug build.') real_price = '$1.23' @@ -64,8 +64,8 @@ class SpecialOfferWindow(ba.Window): return # This can pop up suddenly, so lets block input for 1 second. - _ba.lock_all_input() - ba.timer(1.0, _ba.unlock_all_input, timetype=ba.TimeType.REAL) + ba.internal.lock_all_input() + ba.timer(1.0, ba.internal.unlock_all_input, timetype=ba.TimeType.REAL) ba.playsound(ba.getsound('ding')) ba.timer(0.3, lambda: ba.playsound(ba.getsound('ooh')), @@ -83,10 +83,10 @@ class SpecialOfferWindow(ba.Window): self._is_bundle_sale = False try: if offer['item'] in ['pro', 'pro_fullprice']: - original_price_str = _ba.get_price('pro') + original_price_str = ba.internal.get_price('pro') if original_price_str is None: original_price_str = '?' - new_price_str = _ba.get_price('pro_sale') + new_price_str = ba.internal.get_price('pro_sale') if new_price_str is None: new_price_str = '?' percent_off_text = '' @@ -95,7 +95,7 @@ class SpecialOfferWindow(ba.Window): if ('bonusTickets' in offer and offer['bonusTickets'] is not None): self._is_bundle_sale = True - original_price = _ba.get_v1_account_misc_read_val( + original_price = ba.internal.get_v1_account_misc_read_val( 'price.' + self._offer_item, 9999) # For pure ticket prices we can show a percent-off. @@ -207,7 +207,7 @@ class SpecialOfferWindow(ba.Window): # Total-value if they supplied it. total_worth_item = offer.get('valueItem', None) if total_worth_item is not None: - price = _ba.get_price(total_worth_item) + price = ba.internal.get_price(total_worth_item) total_worth_price = (get_clean_price(price) if price is not None else None) if total_worth_price is not None: @@ -341,10 +341,10 @@ class SpecialOfferWindow(ba.Window): # We go away if we see that our target item is owned. if self._offer_item == 'pro': - if _ba.app.accounts_v1.have_pro(): + if ba.app.accounts_v1.have_pro(): can_die = True else: - if _ba.get_purchased(self._offer_item): + if ba.internal.get_purchased(self._offer_item): can_die = True if can_die: @@ -364,9 +364,9 @@ class SpecialOfferWindow(ba.Window): if not self._root_widget: return sval: str | ba.Lstr - if _ba.get_v1_account_state() == 'signed_in': + if ba.internal.get_v1_account_state() == 'signed_in': sval = (ba.charstr(SpecialChar.TICKET) + - str(_ba.get_v1_account_ticket_count())) + str(ba.internal.get_v1_account_ticket_count())) else: sval = ba.Lstr(resource='getTicketsWindow.titleText') ba.buttonwidget(edit=self._get_tickets_button, label=sval) @@ -374,7 +374,7 @@ class SpecialOfferWindow(ba.Window): def _on_get_more_tickets_press(self) -> None: from bastd.ui import account from bastd.ui import getcurrency - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() return getcurrency.GetCurrencyWindow(modal=True).get_root_widget() @@ -384,16 +384,16 @@ class SpecialOfferWindow(ba.Window): from bastd.ui import getcurrency from bastd.ui import confirm if self._offer['item'] == 'pro': - _ba.purchase('pro_sale') + ba.internal.purchase('pro_sale') elif self._offer['item'] == 'pro_fullprice': - _ba.purchase('pro') + ba.internal.purchase('pro') elif self._is_bundle_sale: # With bundle sales, the price is the name of the IAP. - _ba.purchase(self._offer['price']) + ba.internal.purchase(self._offer['price']) else: ticket_count: int | None try: - ticket_count = _ba.get_v1_account_ticket_count() + ticket_count = ba.internal.get_v1_account_ticket_count() except Exception: ticket_count = None if (ticket_count is not None @@ -403,8 +403,8 @@ class SpecialOfferWindow(ba.Window): return def do_it() -> None: - _ba.in_game_purchase('offer:' + str(self._offer['id']), - self._offer['price']) + ba.internal.in_game_purchase('offer:' + str(self._offer['id']), + self._offer['price']) ba.playsound(ba.getsound('swish')) confirm.ConfirmWindow(ba.Lstr( @@ -446,7 +446,7 @@ def show_offer() -> bool: if app.special_offer.get('item') == 'pro_fullprice': cfg = app.config cfg['pendingSpecialOffer'] = { - 'a': _ba.get_public_login_id(), + 'a': ba.internal.get_public_login_id(), 'o': app.special_offer } cfg.commit() diff --git a/dist/ba_data/python/bastd/ui/store/browser.py b/dist/ba_data/python/bastd/ui/store/browser.py index 042a225..e2471b7 100644 --- a/dist/ba_data/python/bastd/ui/store/browser.py +++ b/dist/ba_data/python/bastd/ui/store/browser.py @@ -10,8 +10,8 @@ import weakref from enum import Enum from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: from typing import Any, Callable, Sequence @@ -271,9 +271,10 @@ class StoreBrowserWindow(ba.Window): def _update_get_tickets_button_pos(self) -> None: uiscale = ba.app.ui.uiscale - pos = (self._width - 252 - (self._x_inset + - (47 if uiscale is ba.UIScale.SMALL - and _ba.is_party_icon_visible() else 0)), + pos = (self._width - 252 - + (self._x_inset + + (47 if uiscale is ba.UIScale.SMALL + and ba.internal.is_party_icon_visible() else 0)), self._height - 70) if self._get_tickets_button: ba.buttonwidget(edit=self._get_tickets_button, position=pos) @@ -282,10 +283,10 @@ class StoreBrowserWindow(ba.Window): def _restore_purchases(self) -> None: from bastd.ui import account - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() else: - _ba.restore_purchases() + ba.internal.restore_purchases() def _update_tabs(self) -> None: from ba.internal import (get_available_sale_time, @@ -323,9 +324,9 @@ class StoreBrowserWindow(ba.Window): if not self._root_widget: return sval: str | ba.Lstr - if _ba.get_v1_account_state() == 'signed_in': + if ba.internal.get_v1_account_state() == 'signed_in': sval = ba.charstr(SpecialChar.TICKET) + str( - _ba.get_v1_account_ticket_count()) + ba.internal.get_v1_account_ticket_count()) else: sval = ba.Lstr(resource='getTicketsWindow.titleText') if self._get_tickets_button: @@ -410,7 +411,7 @@ class StoreBrowserWindow(ba.Window): else: if is_ticket_purchase: if result['allow']: - price = _ba.get_v1_account_misc_read_val( + price = ba.internal.get_v1_account_misc_read_val( 'price.' + item, None) if (price is None or not isinstance(price, int) or price <= 0): @@ -419,7 +420,7 @@ class StoreBrowserWindow(ba.Window): ba.playsound(ba.getsound('error')) else: ba.playsound(ba.getsound('click01')) - _ba.in_game_purchase(item, price) + ba.internal.in_game_purchase(item, price) else: if result['reason'] == 'versionTooOld': ba.playsound(ba.getsound('error')) @@ -434,7 +435,7 @@ class StoreBrowserWindow(ba.Window): # Real in-app purchase. else: if result['allow']: - _ba.purchase(item) + ba.internal.purchase(item) else: if result['reason'] == 'versionTooOld': ba.playsound(ba.getsound('error')) @@ -485,7 +486,7 @@ class StoreBrowserWindow(ba.Window): self._last_buy_time) < 2.0: ba.playsound(ba.getsound('error')) else: - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() else: self._last_buy_time = curtime @@ -499,9 +500,9 @@ class StoreBrowserWindow(ba.Window): self._do_purchase_check('pro' if get_available_sale_time( 'extras') is None else 'pro_sale') else: - price = _ba.get_v1_account_misc_read_val( + price = ba.internal.get_v1_account_misc_read_val( 'price.' + item, None) - our_tickets = _ba.get_v1_account_ticket_count() + our_tickets = ba.internal.get_v1_account_ticket_count() if price is not None and our_tickets < price: ba.playsound(ba.getsound('error')) getcurrency.show_get_tickets_prompt() @@ -540,7 +541,7 @@ class StoreBrowserWindow(ba.Window): if not self._root_widget: return import datetime - sales_raw = _ba.get_v1_account_misc_read_val('sales', {}) + sales_raw = ba.internal.get_v1_account_misc_read_val('sales', {}) sales = {} try: # Look at the current set of sales; filter any with time remaining. @@ -559,9 +560,9 @@ class StoreBrowserWindow(ba.Window): for b_type, b_info in self.button_infos.items(): if b_type in ['upgrades.pro', 'pro']: - purchased = _ba.app.accounts_v1.have_pro() + purchased = ba.app.accounts_v1.have_pro() else: - purchased = _ba.get_purchased(b_type) + purchased = ba.internal.get_purchased(b_type) sale_opacity = 0.0 sale_title_text: str | ba.Lstr = '' @@ -587,10 +588,10 @@ class StoreBrowserWindow(ba.Window): if b_type in ['upgrades.pro', 'pro']: sale_time = get_available_sale_time('extras') if sale_time is not None: - priceraw = _ba.get_price('pro') + priceraw = ba.internal.get_price('pro') price_text_left = (priceraw if priceraw is not None else '?') - priceraw = _ba.get_price('pro_sale') + priceraw = ba.internal.get_price('pro_sale') price_text_right = (priceraw if priceraw is not None else '?') sale_opacity = 1.0 @@ -601,20 +602,20 @@ class StoreBrowserWindow(ba.Window): centi=False, timeformat=ba.TimeFormat.MILLISECONDS) else: - priceraw = _ba.get_price('pro') + priceraw = ba.internal.get_price('pro') price_text = priceraw if priceraw is not None else '?' price_text_left = '' price_text_right = '' else: - price = _ba.get_v1_account_misc_read_val( + price = ba.internal.get_v1_account_misc_read_val( 'price.' + b_type, 0) # Color the button differently if we cant afford this. - if _ba.get_v1_account_state() == 'signed_in': - if _ba.get_v1_account_ticket_count() < price: + if ba.internal.get_v1_account_state() == 'signed_in': + if ba.internal.get_v1_account_ticket_count() < price: color = (0.6, 0.61, 0.6) price_text = ba.charstr(ba.SpecialChar.TICKET) + str( - _ba.get_v1_account_misc_read_val( + ba.internal.get_v1_account_misc_read_val( 'price.' + b_type, '?')) price_text_left = '' price_text_right = '' @@ -1064,7 +1065,7 @@ class StoreBrowserWindow(ba.Window): # pylint: disable=cyclic-import from bastd.ui.account import show_sign_in_prompt from bastd.ui.getcurrency import GetCurrencyWindow - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return self._save_state() diff --git a/dist/ba_data/python/bastd/ui/store/button.py b/dist/ba_data/python/bastd/ui/store/button.py index b766464..b9d4bf8 100644 --- a/dist/ba_data/python/bastd/ui/store/button.py +++ b/dist/ba_data/python/bastd/ui/store/button.py @@ -5,8 +5,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba +import ba.internal if TYPE_CHECKING: from typing import Any, Sequence, Callable @@ -145,7 +145,7 @@ class StoreButton: self._update() def _on_activate(self) -> None: - _ba.increment_analytics_count('Store button press') + ba.internal.increment_analytics_count('Store button press') self._on_activate_call() def set_position(self, position: Sequence[float]) -> None: @@ -197,7 +197,7 @@ class StoreButton: # pylint: disable=cyclic-import from bastd.ui.account import show_sign_in_prompt from bastd.ui.store.browser import StoreBrowserWindow - if _ba.get_v1_account_state() != 'signed_in': + if ba.internal.get_v1_account_state() != 'signed_in': show_sign_in_prompt() return StoreBrowserWindow(modal=True, origin_widget=self._button) @@ -216,9 +216,9 @@ class StoreButton: return # Our instance may outlive our UI objects. if self._ticket_text is not None: - if _ba.get_v1_account_state() == 'signed_in': + if ba.internal.get_v1_account_state() == 'signed_in': sval = ba.charstr(SpecialChar.TICKET) + str( - _ba.get_v1_account_ticket_count()) + ba.internal.get_v1_account_ticket_count()) else: sval = '-' ba.textwidget(edit=self._ticket_text, text=sval) @@ -230,13 +230,13 @@ class StoreButton: # ..also look for new style sales. if sale_time is None: import datetime - sales_raw = _ba.get_v1_account_misc_read_val('sales', {}) + sales_raw = ba.internal.get_v1_account_misc_read_val('sales', {}) sale_times = [] try: # Look at the current set of sales; filter any with time # remaining that we don't own. for sale_item, sale_info in list(sales_raw.items()): - if not _ba.get_purchased(sale_item): + if not ba.internal.get_purchased(sale_item): to_end = (datetime.datetime.utcfromtimestamp( sale_info['e']) - datetime.datetime.utcnow()).total_seconds() diff --git a/dist/ba_data/python/bastd/ui/store/item.py b/dist/ba_data/python/bastd/ui/store/item.py index 5a98edd..fb00ad4 100644 --- a/dist/ba_data/python/bastd/ui/store/item.py +++ b/dist/ba_data/python/bastd/ui/store/item.py @@ -5,7 +5,6 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba if TYPE_CHECKING: @@ -202,7 +201,7 @@ def instantiate_store_item_display(item_name: str, color=(1, 1, 1), texture=ba.gettexture('ticketsMore'))) bonus_tickets = str( - _ba.get_v1_account_misc_read_val('proBonusTickets', 100)) + ba.internal.get_v1_account_misc_read_val('proBonusTickets', 100)) extra_texts.append( ba.textwidget(parent=parent_widget, draw_controller=btn, @@ -270,11 +269,11 @@ def instantiate_store_item_display(item_name: str, # If we have a 'total-worth' item-id for this id, show that price so # the user knows how much this is worth. - total_worth_item = _ba.get_v1_account_misc_read_val('twrths', - {}).get(item_name) + total_worth_item = ba.internal.get_v1_account_misc_read_val( + 'twrths', {}).get(item_name) total_worth_price: str | None if total_worth_item is not None: - price = _ba.get_price(total_worth_item) + price = ba.internal.get_price(total_worth_item) total_worth_price = (get_clean_price(price) if price is not None else '??') else: diff --git a/dist/ba_data/python/bastd/ui/telnet.py b/dist/ba_data/python/bastd/ui/telnet.py index 8ea63e4..0c7080c 100644 --- a/dist/ba_data/python/bastd/ui/telnet.py +++ b/dist/ba_data/python/bastd/ui/telnet.py @@ -4,8 +4,8 @@ from __future__ import annotations -import _ba import ba +import ba.internal class TelnetAccessRequestWindow(ba.Window): @@ -45,9 +45,9 @@ class TelnetAccessRequestWindow(ba.Window): def _cancel(self) -> None: ba.containerwidget(edit=self._root_widget, transition='out_right') - _ba.set_telnet_access_enabled(False) + ba.internal.set_telnet_access_enabled(False) def _ok(self) -> None: ba.containerwidget(edit=self._root_widget, transition='out_left') - _ba.set_telnet_access_enabled(True) + ba.internal.set_telnet_access_enabled(True) ba.screenmessage(ba.Lstr(resource='telnetAccessGrantedText')) diff --git a/dist/ba_data/python/bastd/ui/tournamententry.py b/dist/ba_data/python/bastd/ui/tournamententry.py index a495792..f543755 100644 --- a/dist/ba_data/python/bastd/ui/tournamententry.py +++ b/dist/ba_data/python/bastd/ui/tournamententry.py @@ -6,8 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba +import ba.internal from bastd.ui import popup if TYPE_CHECKING: @@ -88,7 +88,7 @@ class TournamentEntryWindow(popup.PopupWindow): self._launched = False # Show the ad button only if we support ads *and* it has a level 1 fee. - self._do_ad_btn = (_ba.has_video_ads() and self._allow_ads) + self._do_ad_btn = (ba.internal.has_video_ads() and self._allow_ads) x_offs = 0 if self._do_ad_btn else 85 @@ -349,13 +349,13 @@ class TournamentEntryWindow(popup.PopupWindow): if not self._running_query and ( (self._last_query_time is None) or (not self._have_valid_data) or (ba.time(ba.TimeType.REAL) - self._last_query_time > 30.0)): - _ba.tournament_query(args={ - 'source': - 'entry window' if self._tournament_activity is None else - 'retry entry window' - }, - callback=ba.WeakCall( - self._on_tournament_query_response)) + ba.internal.tournament_query( + args={ + 'source': + 'entry window' if self._tournament_activity is None + else 'retry entry window' + }, + callback=ba.WeakCall(self._on_tournament_query_response)) self._last_query_time = ba.time(ba.TimeType.REAL) self._running_query = True @@ -376,7 +376,7 @@ class TournamentEntryWindow(popup.PopupWindow): timeformat=ba.TimeFormat.MILLISECONDS)) # Keep price up-to-date and update the button with it. - self._purchase_price = _ba.get_v1_account_misc_read_val( + self._purchase_price = ba.internal.get_v1_account_misc_read_val( self._purchase_price_name, None) ba.textwidget( @@ -403,7 +403,7 @@ class TournamentEntryWindow(popup.PopupWindow): if self._purchase_price == 0 else self._ticket_img_pos) if self._do_ad_btn: - enabled = _ba.have_incentivized_ad() + enabled = ba.internal.have_incentivized_ad() have_ad_tries_remaining = ( self._tournament_info['adTriesRemaining'] is not None and self._tournament_info['adTriesRemaining'] > 0) @@ -424,7 +424,7 @@ class TournamentEntryWindow(popup.PopupWindow): color=(0, 0.8, 0) if enabled else (0.4, 0.4, 0.4)) try: - t_str = str(_ba.get_v1_account_ticket_count()) + t_str = str(ba.internal.get_v1_account_ticket_count()) except Exception: t_str = '?' if self._get_tickets_button: @@ -514,7 +514,7 @@ class TournamentEntryWindow(popup.PopupWindow): # Deny if we don't have enough tickets. ticket_count: int | None try: - ticket_count = _ba.get_v1_account_ticket_count() + ticket_count = ba.internal.get_v1_account_ticket_count() except Exception: # FIXME: should add a ba.NotSignedInError we can use here. ticket_count = None @@ -527,15 +527,15 @@ class TournamentEntryWindow(popup.PopupWindow): cur_time = ba.time(ba.TimeType.REAL, ba.TimeFormat.MILLISECONDS) self._last_ticket_press_time = cur_time assert isinstance(ticket_cost, int) - _ba.in_game_purchase(self._purchase_name, ticket_cost) + ba.internal.in_game_purchase(self._purchase_name, ticket_cost) self._entering = True - _ba.add_transaction({ + ba.internal.add_transaction({ 'type': 'ENTER_TOURNAMENT', 'fee': self._fee, 'tournamentID': self._tournament_id }) - _ba.run_transactions() + ba.internal.run_transactions() self._launch() def _on_pay_with_ad_press(self) -> None: @@ -560,15 +560,15 @@ class TournamentEntryWindow(popup.PopupWindow): cur_time = ba.time(ba.TimeType.REAL) if cur_time - self._last_ad_press_time > 5.0: self._last_ad_press_time = cur_time - _ba.app.ads.show_ad_2('tournament_entry', - on_completion_call=ba.WeakCall( - self._on_ad_complete)) + ba.app.ads.show_ad_2('tournament_entry', + on_completion_call=ba.WeakCall( + self._on_ad_complete)) def _on_ad_complete(self, actually_showed: bool) -> None: # Make sure any transactions the ad added got locally applied # (rewards added, etc.). - _ba.run_transactions() + ba.internal.run_transactions() # If we're already entering the tourney, ignore. if self._entering: @@ -580,19 +580,19 @@ class TournamentEntryWindow(popup.PopupWindow): # This should have awarded us the tournament_entry_ad purchase; # make sure that's present. # (otherwise the server will ignore our tournament entry anyway) - if not _ba.get_purchased('tournament_entry_ad'): + if not ba.internal.get_purchased('tournament_entry_ad'): print('no tournament_entry_ad purchase present in _on_ad_complete') ba.screenmessage(ba.Lstr(resource='errorText'), color=(1, 0, 0)) ba.playsound(ba.getsound('error')) return self._entering = True - _ba.add_transaction({ + ba.internal.add_transaction({ 'type': 'ENTER_TOURNAMENT', 'fee': 'ad', 'tournamentID': self._tournament_id }) - _ba.run_transactions() + ba.internal.run_transactions() self._launch() def _on_get_tickets_press(self) -> None: @@ -614,9 +614,10 @@ class TournamentEntryWindow(popup.PopupWindow): # button if it looks like we're waiting on a purchase or entering # the tournament. if ((ba.time(ba.TimeType.REAL, ba.TimeFormat.MILLISECONDS) - - self._last_ticket_press_time < 6000) and - (_ba.have_outstanding_transactions() - or _ba.get_purchased(self._purchase_name) or self._entering)): + self._last_ticket_press_time < 6000) + and (ba.internal.have_outstanding_transactions() + or ba.internal.get_purchased(self._purchase_name) + or self._entering)): ba.playsound(ba.getsound('error')) return self._transition_out() diff --git a/dist/ba_data/python/bastd/ui/tournamentscores.py b/dist/ba_data/python/bastd/ui/tournamentscores.py index 609bde0..3662512 100644 --- a/dist/ba_data/python/bastd/ui/tournamentscores.py +++ b/dist/ba_data/python/bastd/ui/tournamentscores.py @@ -6,8 +6,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -import _ba import ba +import ba.internal from bastd.ui import popup as popup_ui if TYPE_CHECKING: @@ -99,13 +99,13 @@ class TournamentScoresWindow(popup_ui.PopupWindow): ba.containerwidget(edit=self.root_widget, cancel_button=self._cancel_button) - _ba.tournament_query(args={ + ba.internal.tournament_query(args={ 'tournamentIDs': [tournament_id], 'numScores': 50, 'source': 'scores window' }, - callback=ba.WeakCall( - self._on_tournament_query_response)) + callback=ba.WeakCall( + self._on_tournament_query_response)) def _on_tournament_query_response(self, data: dict[str, Any] | None) -> None: diff --git a/dist/ba_data/python/bastd/ui/url.py b/dist/ba_data/python/bastd/ui/url.py index 00f3b30..fe90fb3 100644 --- a/dist/ba_data/python/bastd/ui/url.py +++ b/dist/ba_data/python/bastd/ui/url.py @@ -4,8 +4,8 @@ from __future__ import annotations -import _ba import ba +import ba.internal class ShowURLWindow(ba.Window): @@ -41,7 +41,7 @@ class ShowURLWindow(ba.Window): position=(self._width * 0.5 - qr_size * 0.5, self._height * 0.5 - qr_size * 0.5), size=(qr_size, qr_size), - texture=_ba.get_qrcode_texture(address)) + texture=ba.internal.get_qrcode_texture(address)) ba.containerwidget(edit=self._root_widget, cancel_button=self._cancel_button) else: diff --git a/dist/ba_data/python/bastd/ui/watch.py b/dist/ba_data/python/bastd/ui/watch.py index 296bc36..eca946b 100644 --- a/dist/ba_data/python/bastd/ui/watch.py +++ b/dist/ba_data/python/bastd/ui/watch.py @@ -8,8 +8,8 @@ import os from enum import Enum from typing import TYPE_CHECKING, cast -import _ba import ba +import ba.internal if TYPE_CHECKING: from typing import Any @@ -114,10 +114,11 @@ class WatchWindow(ba.Window): if ba.app.ui.use_toolbars: first_tab = self._tab_row.tabs[tabdefs[0][0]] last_tab = self._tab_row.tabs[tabdefs[-1][0]] - ba.widget(edit=last_tab.button, - right_widget=_ba.get_special_widget('party_button')) + ba.widget( + edit=last_tab.button, + right_widget=ba.internal.get_special_widget('party_button')) if uiscale is ba.UIScale.SMALL: - bbtn = _ba.get_special_widget('back_button') + bbtn = ba.internal.get_special_widget('back_button') ba.widget(edit=first_tab.button, up_widget=bbtn, left_widget=bbtn) @@ -224,8 +225,9 @@ class WatchWindow(ba.Window): autoselect=True) ba.widget(edit=btn1, up_widget=self._tab_row.tabs[tab_id].button) if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: - ba.widget(edit=btn1, - left_widget=_ba.get_special_widget('back_button')) + ba.widget( + edit=btn1, + left_widget=ba.internal.get_special_widget('back_button')) btnv -= b_height + b_space_extra ba.buttonwidget(parent=cnt, size=(b_width, b_height), @@ -282,25 +284,25 @@ class WatchWindow(ba.Window): if self._my_replay_selected is None: self._no_replay_selected_error() return - _ba.increment_analytics_count('Replay watch') + ba.internal.increment_analytics_count('Replay watch') def do_it() -> None: try: # Reset to normal speed. - _ba.set_replay_speed_exponent(0) - _ba.fade_screen(True) + ba.internal.set_replay_speed_exponent(0) + ba.internal.fade_screen(True) assert self._my_replay_selected is not None - _ba.new_replay_session(_ba.get_replays_dir() + '/' + - self._my_replay_selected) + ba.internal.new_replay_session(ba.internal.get_replays_dir() + + '/' + self._my_replay_selected) except Exception: ba.print_exception('Error running replay session.') # Drop back into a fresh main menu session # in case we half-launched or something. from bastd import mainmenu - _ba.new_host_session(mainmenu.MainMenuSession) + ba.internal.new_host_session(mainmenu.MainMenuSession) - _ba.fade_screen(False, endcall=ba.Call(ba.pushcall, do_it)) + ba.internal.fade_screen(False, endcall=ba.Call(ba.pushcall, do_it)) ba.containerwidget(edit=self._root_widget, transition='out_left') def _on_my_replay_rename_press(self) -> None: @@ -371,9 +373,9 @@ class WatchWindow(ba.Window): # (or what it looks like to the user). if (replay != new_name and self._get_replay_display_name(replay) != new_name_raw): - old_name_full = (_ba.get_replays_dir() + '/' + + old_name_full = (ba.internal.get_replays_dir() + '/' + replay).encode('utf-8') - new_name_full = (_ba.get_replays_dir() + '/' + + new_name_full = (ba.internal.get_replays_dir() + '/' + new_name).encode('utf-8') # False alarm; ba.textwidget can return non-None val. # pylint: disable=unsupported-membership-test @@ -389,7 +391,7 @@ class WatchWindow(ba.Window): '.replayRenameErrorInvalidName'), color=(1, 0, 0)) else: - _ba.increment_analytics_count('Replay rename') + ba.internal.increment_analytics_count('Replay rename') os.rename(old_name_full, new_name_full) self._refresh_my_replays() ba.playsound(ba.getsound('gunCocking')) @@ -426,8 +428,9 @@ class WatchWindow(ba.Window): def _delete_replay(self, replay: str) -> None: try: - _ba.increment_analytics_count('Replay delete') - os.remove((_ba.get_replays_dir() + '/' + replay).encode('utf-8')) + ba.internal.increment_analytics_count('Replay delete') + os.remove( + (ba.internal.get_replays_dir() + '/' + replay).encode('utf-8')) self._refresh_my_replays() ba.playsound(ba.getsound('shieldDown')) if replay == self._my_replay_selected: @@ -449,7 +452,7 @@ class WatchWindow(ba.Window): child.delete() t_scale = 1.6 try: - names = os.listdir(_ba.get_replays_dir()) + names = os.listdir(ba.internal.get_replays_dir()) # Ignore random other files in there. names = [n for n in names if n.endswith('.brp')] diff --git a/dist/ba_data/python/efro/error.py b/dist/ba_data/python/efro/error.py index 640d014..d33c42d 100644 --- a/dist/ba_data/python/efro/error.py +++ b/dist/ba_data/python/efro/error.py @@ -188,7 +188,14 @@ def is_asyncio_streams_communication_error(exc: BaseException) -> bool: # Let's still complain, however, if we get any SSL errors besides # this one. https://bugs.python.org/issue39951 if isinstance(exc, ssl.SSLError): - if 'APPLICATION_DATA_AFTER_CLOSE_NOTIFY' in str(exc): + excstr = str(exc) + if 'APPLICATION_DATA_AFTER_CLOSE_NOTIFY' in excstr: + return True + + # Also occasionally am getting WRONG_VERSION_NUMBER ssl errors; + # Assuming this just means client is attempting to connect from some + # outdated browser or whatnot. + if 'SSL: WRONG_VERSION_NUMBER' in excstr: return True return False diff --git a/dist/ba_data/python/efro/log.py b/dist/ba_data/python/efro/log.py new file mode 100644 index 0000000..854fcc7 --- /dev/null +++ b/dist/ba_data/python/efro/log.py @@ -0,0 +1,438 @@ +# Released under the MIT License. See LICENSE for details. +# +"""Logging functionality.""" +from __future__ import annotations + +import sys +import time +import asyncio +import logging +import datetime +from enum import Enum +from dataclasses import dataclass +from typing import TYPE_CHECKING, Annotated +from threading import Thread, current_thread, Lock + +from efro.util import utc_now +from efro.call import tpartial +from efro.terminal import TerminalColor +from efro.dataclassio import ioprepped, IOAttrs, dataclass_to_json + +if TYPE_CHECKING: + from pathlib import Path + from typing import Any, Callable, TextIO + + +class LogLevel(Enum): + """Severity level for a log entry. + + These enums have numeric values so they can be compared in severity. + Note that these values are not currently interchangeable with the + logging.ERROR, logging.DEBUG, etc. values. + """ + DEBUG = 0 + INFO = 1 + WARNING = 2 + ERROR = 3 + CRITICAL = 4 + + +LEVELNO_LOG_LEVELS = { + logging.DEBUG: LogLevel.DEBUG, + logging.INFO: LogLevel.INFO, + logging.WARNING: LogLevel.WARNING, + logging.ERROR: LogLevel.ERROR, + logging.CRITICAL: LogLevel.CRITICAL +} + +LEVELNO_COLOR_CODES: dict[int, tuple[str, str]] = { + logging.DEBUG: (TerminalColor.CYAN.value, TerminalColor.RESET.value), + logging.INFO: ('', ''), + logging.WARNING: (TerminalColor.YELLOW.value, TerminalColor.RESET.value), + logging.ERROR: (TerminalColor.RED.value, TerminalColor.RESET.value), + logging.CRITICAL: + (TerminalColor.STRONG_MAGENTA.value + TerminalColor.BOLD.value + + TerminalColor.BG_BLACK.value, TerminalColor.RESET.value), +} + + +@ioprepped +@dataclass +class LogEntry: + """Single logged message.""" + name: Annotated[str, + IOAttrs('n', soft_default='root', store_default=False)] + message: Annotated[str, IOAttrs('m')] + level: Annotated[LogLevel, IOAttrs('l')] + time: Annotated[datetime.datetime, IOAttrs('t')] + + +@ioprepped +@dataclass +class LogArchive: + """Info and data for a log.""" + + # Total number of entries submitted to the log. + log_size: Annotated[int, IOAttrs('t')] + + # Offset for the entries contained here. + # (10 means our first entry is the 10th in the log, etc.) + start_index: Annotated[int, IOAttrs('c')] + + entries: Annotated[list[LogEntry], IOAttrs('e')] + + +class LogHandler(logging.Handler): + """Fancy-pants handler for logging output. + + Writes logs to disk in structured json format and echoes them + to stdout/stderr with pretty colors. + """ + + _event_loop: asyncio.AbstractEventLoop + + # IMPORTANT: Any debug prints we do here should ONLY go to echofile. + # Otherwise we can get infinite loops as those prints come back to us + # as new log entries. + + def __init__(self, + path: str | Path | None, + echofile: TextIO | None, + suppress_non_root_debug: bool = False, + cache_size_limit: int = 0): + super().__init__() + # pylint: disable=consider-using-with + self._file = (None + if path is None else open(path, 'w', encoding='utf-8')) + self._echofile = echofile + self._callbacks_lock = Lock() + self._callbacks: list[Callable[[LogEntry], None]] = [] + self._suppress_non_root_debug = suppress_non_root_debug + self._file_chunks: dict[str, list[str]] = {'stdout': [], 'stderr': []} + self._file_chunk_ship_task: dict[str, asyncio.Task | None] = { + 'stdout': None, + 'stderr': None + } + self._cache_size = 0 + assert cache_size_limit >= 0 + self._cache_size_limit = cache_size_limit + self._cache: list[tuple[int, LogEntry]] = [] + self._cache_index_offset = 0 + self._cache_lock = Lock() + self._printed_callback_error = False + self._thread_bootstrapped = False + self._thread = Thread(target=self._thread_main, daemon=True) + self._thread.start() + + # Spin until our thread is up and running; otherwise we could + # wind up trying to push stuff to our event loop before the + # loop exists. + while not self._thread_bootstrapped: + time.sleep(0.001) + + def add_callback(self, call: Callable[[LogEntry], None]) -> None: + """Add a callback to be run for each LogEntry. + + Note that this callback will always run in a background thread. + """ + with self._callbacks_lock: + self._callbacks.append(call) + + def _thread_main(self) -> None: + self._event_loop = asyncio.new_event_loop() + # NOTE: if we ever use default threadpool at all we should allow + # setting it for our loop. + asyncio.set_event_loop(self._event_loop) + self._thread_bootstrapped = True + try: + self._event_loop.run_forever() + except BaseException: + # If this ever goes down we're in trouble. + # We won't be able to log about it though... + # Try to make some noise however we can. + print('LogHandler died!!!', file=sys.stderr) + import traceback + traceback.print_exc() + raise + + def get_cached(self, + start_index: int = 0, + max_entries: int | None = None) -> LogArchive: + """Build and return an archive of cached log entries. + + This will only include entries that have been processed by the + background thread, so may not include just-submitted logs or + entries for partially written stdout/stderr lines. + Entries from the range [start_index:start_index+max_entries] + which are still present in the cache will be returned. + """ + + assert start_index >= 0 + if max_entries is not None: + assert max_entries >= 0 + with self._cache_lock: + # Transform start_index to our present cache space. + start_index -= self._cache_index_offset + # Calc end-index in our present cache space. + end_index = (len(self._cache) + if max_entries is None else start_index + max_entries) + + # Clamp both indexes to both ends of our present space. + start_index = max(0, min(start_index, len(self._cache))) + end_index = max(0, min(end_index, len(self._cache))) + + return LogArchive( + log_size=self._cache_index_offset + len(self._cache), + start_index=start_index + self._cache_index_offset, + entries=[e[1] for e in self._cache[start_index:end_index]]) + + def emit(self, record: logging.LogRecord) -> None: + # Called by logging to send us records. + # We simply package them up and ship them to our thread. + # UPDATE: turns out we CAN get log messages from this thread + # (the C++ layer can spit out some performance metrics when + # calls take too long/etc.) + # assert current_thread() is not self._thread + + # Special case - filter out this common extra-chatty category. + # TODO - should use a standard logging.Filter for this. + if (self._suppress_non_root_debug and record.name != 'root' + and record.levelname == 'DEBUG'): + return + + # We want to forward as much as we can along without processing it + # (better to do so in a bg thread). + # However its probably best to flatten the message string here since + # it could cause problems stringifying things in threads where they + # didn't expect to be stringified. + msg = self.format(record) + + # Also immediately print pretty colored output to our echo file + # (generally stderr). We do this part here instead of in our bg + # thread because the delay can throw off command line prompts or + # make tight debugging harder. + if self._echofile is not None: + ends = LEVELNO_COLOR_CODES.get(record.levelno) + if ends is not None: + self._echofile.write(f'{ends[0]}{msg}{ends[1]}\n') + else: + self._echofile.write(f'{msg}\n') + + self._event_loop.call_soon_threadsafe( + tpartial(self._emit_in_thread, record.name, record.levelno, + record.created, msg)) + + def _emit_in_thread(self, name: str, levelno: int, created: float, + message: str) -> None: + try: + self._emit_entry( + LogEntry(name=name, + message=message, + level=LEVELNO_LOG_LEVELS.get(levelno, LogLevel.INFO), + time=datetime.datetime.fromtimestamp( + created, datetime.timezone.utc))) + except Exception: + import traceback + traceback.print_exc(file=self._echofile) + + def file_write(self, name: str, output: str) -> None: + """Send raw stdout/stderr output to the logger to be collated.""" + + self._event_loop.call_soon_threadsafe( + tpartial(self._file_write_in_thread, name, output)) + + def _file_write_in_thread(self, name: str, output: str) -> None: + try: + assert name in ('stdout', 'stderr') + + # Here we try to be somewhat smart about breaking arbitrary + # print output into discrete log entries. + + self._file_chunks[name].append(output) + + # Individual parts of a print come across as separate writes, + # and the end of a print will be a standalone '\n' by default. + # Let's use that as a hint that we're likely at the end of + # a full print statement and ship what we've got. + if output == '\n': + self._ship_file_chunks(name, cancel_ship_task=True) + else: + # By default just keep adding chunks. + # However we keep a timer running anytime we've got + # unshipped chunks so that we can ship what we've got + # after a short bit if we never get a newline. + ship_task = self._file_chunk_ship_task[name] + if ship_task is None: + self._file_chunk_ship_task[name] = ( + self._event_loop.create_task( + self._ship_chunks_task(name))) + + except Exception: + import traceback + traceback.print_exc(file=self._echofile) + + def file_flush(self, name: str) -> None: + """Send raw stdout/stderr flush to the logger to be collated.""" + + self._event_loop.call_soon_threadsafe( + tpartial(self._file_flush_in_thread, name)) + + def _file_flush_in_thread(self, name: str) -> None: + try: + assert name in ('stdout', 'stderr') + + # Immediately ship whatever chunks we've got. + if self._file_chunks[name]: + self._ship_file_chunks(name, cancel_ship_task=True) + + except Exception: + import traceback + traceback.print_exc(file=self._echofile) + + async def _ship_chunks_task(self, name: str) -> None: + await asyncio.sleep(0.1) + self._ship_file_chunks(name, cancel_ship_task=False) + + def _ship_file_chunks(self, name: str, cancel_ship_task: bool) -> None: + # Note: Raw print input generally ends in a newline, but that is + # redundant when we break things into log entries and results + # in extra empty lines. So strip off a single trailing newline. + text = ''.join(self._file_chunks[name]).removesuffix('\n') + + self._emit_entry( + LogEntry(name=name, + message=text, + level=LogLevel.INFO, + time=utc_now())) + self._file_chunks[name] = [] + ship_task = self._file_chunk_ship_task[name] + if cancel_ship_task and ship_task is not None: + ship_task.cancel() + self._file_chunk_ship_task[name] = None + + def _emit_entry(self, entry: LogEntry) -> None: + assert current_thread() is self._thread + + # Store to our cache. + if self._cache_size_limit > 0: + with self._cache_lock: + # Do a rough calc of how many bytes this entry consumes. + entry_size = sum( + sys.getsizeof(x) + for x in (entry, entry.name, entry.message, entry.level, + entry.time)) + self._cache.append((entry_size, entry)) + self._cache_size += entry_size + + # Prune old until we are back at or under our limit. + while self._cache_size > self._cache_size_limit: + popped = self._cache.pop(0) + self._cache_size -= popped[0] + self._cache_index_offset += 1 + + # Pass to callbacks. + with self._callbacks_lock: + for call in self._callbacks: + try: + call(entry) + except Exception: + # Only print one callback error to avoid insanity. + if not self._printed_callback_error: + import traceback + traceback.print_exc(file=self._echofile) + self._printed_callback_error = True + + # Dump to our structured log file. + # TODO: set a timer for flushing; don't flush every line. + if self._file is not None: + entry_s = dataclass_to_json(entry) + assert '\n' not in entry_s # Make sure its a single line. + print(entry_s, file=self._file, flush=True) + + +class FileLogEcho: + """A file-like object for forwarding stdout/stderr to a LogHandler.""" + + def __init__(self, original: TextIO, name: str, + handler: LogHandler) -> None: + assert name in ('stdout', 'stderr') + self._original = original + self._name = name + self._handler = handler + + def write(self, output: Any) -> None: + """Override standard write call.""" + self._original.write(output) + self._handler.file_write(self._name, output) + + def flush(self) -> None: + """Flush the file.""" + self._original.flush() + + # We also use this as a hint to ship whatever file chunks + # we've accumulated (we have to try and be smart about breaking + # our arbitrary file output into discrete entries). + self._handler.file_flush(self._name) + + def isatty(self) -> bool: + """Are we a terminal?""" + return self._original.isatty() + + +def setup_logging(log_path: str | Path | None, + level: LogLevel, + suppress_non_root_debug: bool = False, + log_stdout_stderr: bool = False, + cache_size_limit: int = 0) -> LogHandler: + """Set up our logging environment. + + Returns the custom handler which can be used to fetch information + about logs that have passed through it. (worst log-levels, caches, etc.). + """ + + lmap = { + LogLevel.DEBUG: logging.DEBUG, + LogLevel.INFO: logging.INFO, + LogLevel.WARNING: logging.WARNING, + LogLevel.ERROR: logging.ERROR, + LogLevel.CRITICAL: logging.CRITICAL, + } + + # Wire logger output to go to a structured log file. + # Also echo it to stderr IF we're running in a terminal. + # UPDATE: Actually gonna always go to stderr. Is there a + # reason we shouldn't? This makes debugging possible if all + # we have is access to a non-interactive terminal or file dump. + # We could add a '--quiet' arg or whatnot to change this behavior. + + # Note: by passing in the *original* stderr here before we + # (potentially) replace it, we ensure that our log echos + # won't themselves be intercepted and sent to the logger + # which would create an infinite loop. + loghandler = LogHandler( + log_path, + # echofile=sys.stderr if sys.stderr.isatty() else None, + echofile=sys.stderr, + suppress_non_root_debug=suppress_non_root_debug, + cache_size_limit=cache_size_limit) + + # Note: going ahead with force=True here so that we replace any + # existing logger. Though we warn if it looks like we are doing + # that so we can try to avoid creating the first one. + had_previous_handlers = bool(logging.root.handlers) + logging.basicConfig(level=lmap[level], + format='%(message)s', + handlers=[loghandler], + force=True) + if had_previous_handlers: + logging.warning('setup_logging: force-replacing previous handlers.') + + # Optionally intercept Python's stdout/stderr output and generate + # log entries from it. + if log_stdout_stderr: + sys.stdout = FileLogEcho( # type: ignore + sys.stdout, 'stdout', loghandler) + sys.stderr = FileLogEcho( # type: ignore + sys.stderr, 'stderr', loghandler) + + return loghandler diff --git a/dist/ba_data/python/efro/message/__init__.py b/dist/ba_data/python/efro/message/__init__.py index 11a82fd..326ba02 100644 --- a/dist/ba_data/python/efro/message/__init__.py +++ b/dist/ba_data/python/efro/message/__init__.py @@ -11,15 +11,17 @@ from efro.message._protocol import MessageProtocol from efro.message._sender import (MessageSender, BoundMessageSender) from efro.message._receiver import (MessageReceiver, BoundMessageReceiver) from efro.message._module import (create_sender_module, create_receiver_module) -from efro.message._message import (Message, Response, EmptyResponse, - ErrorResponse, StringResponse, BoolResponse, +from efro.message._message import (Message, Response, SysResponse, + EmptySysResponse, ErrorSysResponse, + StringResponse, BoolResponse, UnregisteredMessageIDError) __all__ = [ - 'Message', 'Response', 'EmptyResponse', 'ErrorResponse', 'StringResponse', - 'BoolResponse', 'MessageProtocol', 'MessageSender', 'BoundMessageSender', - 'MessageReceiver', 'BoundMessageReceiver', 'create_sender_module', - 'create_receiver_module', 'UnregisteredMessageIDError' + 'Message', 'Response', 'SysResponse', 'EmptySysResponse', + 'ErrorSysResponse', 'StringResponse', 'BoolResponse', 'MessageProtocol', + 'MessageSender', 'BoundMessageSender', 'MessageReceiver', + 'BoundMessageReceiver', 'create_sender_module', 'create_receiver_module', + 'UnregisteredMessageIDError' ] # Have these things present themselves cleanly as 'thismodule.SomeClass' diff --git a/dist/ba_data/python/efro/message/_message.py b/dist/ba_data/python/efro/message/_message.py index 60e108f..7a00070 100644 --- a/dist/ba_data/python/efro/message/_message.py +++ b/dist/ba_data/python/efro/message/_message.py @@ -24,52 +24,56 @@ class Message: """Base class for messages.""" @classmethod - def get_response_types(cls) -> list[type[Response]]: - """Return all message types this Message can result in when sent. + def get_response_types(cls) -> list[type[Response] | None]: + """Return all Response types this Message can return when sent. - The default implementation specifies EmptyResponse, so messages with - no particular response needs can leave this untouched. - Note that ErrorMessage is handled as a special case and does not - need to be specified here. + The default implementation specifies a None return type. """ - return [EmptyResponse] + return [None] class Response: """Base class for responses to messages.""" +class SysResponse: + """Base class for system-responses to messages. + + These are only sent/handled by the messaging system itself; + users of the api never see them. + """ + + # Some standard response types: @ioprepped @dataclass -class ErrorResponse(Response): - """Message saying some error has occurred on the other end. +class ErrorSysResponse(SysResponse): + """SysResponse saying some error has occurred for the send. - This type is unique in that it is not returned to the user; it - instead results in a local exception being raised. + This generally results in an Exception being raised for the caller. """ class ErrorType(Enum): - """Type of error that occurred in remote message handling.""" - OTHER = 0 - CLEAN = 1 + """Type of error that occurred while sending a message.""" + REMOTE = 0 + REMOTE_CLEAN = 1 LOCAL = 2 COMMUNICATION = 3 error_message: Annotated[str, IOAttrs('m')] - error_type: Annotated[ErrorType, IOAttrs('e')] = ErrorType.OTHER + error_type: Annotated[ErrorType, IOAttrs('e')] = ErrorType.REMOTE @ioprepped @dataclass -class EmptyResponse(Response): +class EmptySysResponse(SysResponse): """The response equivalent of None.""" # TODO: could allow handlers to deal in raw values for these -# types similar to how we allow None in place of EmptyResponse. +# types similar to how we allow None in place of EmptySysResponse. # Though not sure if they are widely used enough to warrant the # extra code complexity. @ioprepped diff --git a/dist/ba_data/python/efro/message/_protocol.py b/dist/ba_data/python/efro/message/_protocol.py index e1e5cfc..86387de 100644 --- a/dist/ba_data/python/efro/message/_protocol.py +++ b/dist/ba_data/python/efro/message/_protocol.py @@ -14,8 +14,9 @@ import json from efro.error import CleanError from efro.dataclassio import (is_ioprepped_dataclass, dataclass_to_dict, dataclass_from_dict) -from efro.message._message import (Message, Response, ErrorResponse, - EmptyResponse, UnregisteredMessageIDError) +from efro.message._message import (Message, Response, SysResponse, + ErrorSysResponse, EmptySysResponse, + UnregisteredMessageIDError) if TYPE_CHECKING: from typing import Any, Literal @@ -33,37 +34,30 @@ class MessageProtocol: def __init__(self, message_types: dict[int, type[Message]], response_types: dict[int, type[Response]], - preserve_clean_errors: bool = True, - receiver_logs_exceptions: bool = True, - receiver_returns_stack_traces: bool = False) -> None: + forward_clean_errors: bool = False, + remote_errors_include_stack_traces: bool = False) -> None: """Create a protocol with a given configuration. Note that common response types are automatically registered with (unchanging negative ids) so they don't need to be passed explicitly (but can be if a different id is desired). - If 'preserve_clean_errors' is True, efro.error.CleanError + If 'forward_clean_errors' is True, efro.error.CleanError exceptions raised on the receiver end will result in a matching CleanError raised back on the sender. All other Exception types come across as efro.error.RemoteError. - When 'receiver_logs_exceptions' is True, any uncaught Exceptions - on the receiver end will be logged there via logging.exception() - (in addition to the usual behavior of returning an ErrorResponse - to the sender). This is good to leave enabled if your - intention is to never return ErrorResponses. Looser setups - making routine use of CleanErrors or whatnot may want to - disable this, however. - - If 'receiver_returns_stack_traces' is True, stringified stack + If 'remote_errors_include_stack_traces' is True, stringified stack traces will be returned to the sender for exceptions occurring on the receiver end. This can make debugging easier but should only be used when the client is trusted to see such info. """ self.message_types_by_id: dict[int, type[Message]] = {} self.message_ids_by_type: dict[type[Message], int] = {} - self.response_types_by_id: dict[int, type[Response]] = {} - self.response_ids_by_type: dict[type[Response], int] = {} + self.response_types_by_id: dict[int, type[Response] + | type[SysResponse]] = {} + self.response_ids_by_type: dict[type[Response] | type[SysResponse], + int] = {} for m_id, m_type in message_types.items(): # Make sure only valid message types were passed and each @@ -85,32 +79,33 @@ class MessageProtocol: self.response_types_by_id[r_id] = r_type self.response_ids_by_type[r_type] = r_id - # Go ahead and auto-register a few common response types - # if the user has not done so explicitly. Use unique negative - # IDs which will never change or overlap with user ids. - def _reg_if_not(reg_tp: type[Response], reg_id: int) -> None: - if reg_tp in self.response_ids_by_type: - return + # Register our SysResponse types. These use negative + # IDs so as to never overlap with user Response types. + def _reg_sys(reg_tp: type[SysResponse], reg_id: int) -> None: assert self.response_types_by_id.get(reg_id) is None self.response_types_by_id[reg_id] = reg_tp self.response_ids_by_type[reg_tp] = reg_id - _reg_if_not(ErrorResponse, -1) - _reg_if_not(EmptyResponse, -2) + _reg_sys(ErrorSysResponse, -1) + _reg_sys(EmptySysResponse, -2) # Some extra-thorough validation in debug mode. if __debug__: # Make sure all Message types' return types are valid # and have been assigned an ID as well. - all_response_types: set[type[Response]] = set() + all_response_types: set[type[Response] | None] = set() for m_id, m_type in message_types.items(): m_rtypes = m_type.get_response_types() + assert isinstance(m_rtypes, list) assert m_rtypes, ( f'Message type {m_type} specifies no return types.') assert len(set(m_rtypes)) == len(m_rtypes) # check dups - all_response_types.update(m_rtypes) + for m_rtype in m_rtypes: + all_response_types.add(m_rtype) for cls in all_response_types: + if cls is None: + continue assert is_ioprepped_dataclass(cls) assert issubclass(cls, Response) if cls not in self.response_ids_by_type: @@ -127,9 +122,9 @@ class MessageProtocol: 'message_types contains duplicate __name__s;' ' all types are required to have unique names.') - self.preserve_clean_errors = preserve_clean_errors - self.receiver_logs_exceptions = receiver_logs_exceptions - self.receiver_returns_stack_traces = receiver_returns_stack_traces + self.forward_clean_errors = forward_clean_errors + self.remote_errors_include_stack_traces = ( + remote_errors_include_stack_traces) @staticmethod def encode_dict(obj: dict) -> str: @@ -140,26 +135,27 @@ class MessageProtocol: """Encode a message to a json ready dict.""" return self._to_dict(message, self.message_ids_by_type, 'message') - def response_to_dict(self, response: Response) -> dict: + def response_to_dict(self, response: Response | SysResponse) -> dict: """Encode a response to a json ready dict.""" return self._to_dict(response, self.response_ids_by_type, 'response') - def error_to_response(self, exc: Exception) -> Response: + def error_to_response(self, exc: Exception) -> SysResponse: """Translate an error to a response.""" - # Log any errors we got during handling if so desired. - if self.receiver_logs_exceptions: - logging.exception('Error handling message.') + # Log any errors we got during handling. + logging.exception('Error in efro.message handling.') - # If anything goes wrong, return a ErrorResponse instead. - if isinstance(exc, CleanError) and self.preserve_clean_errors: - return ErrorResponse(error_message=str(exc), - error_type=ErrorResponse.ErrorType.CLEAN) - return ErrorResponse( + # If anything goes wrong, return a ErrorSysResponse instead. + # (either CLEAN or generic REMOTE) + if isinstance(exc, CleanError) and self.forward_clean_errors: + return ErrorSysResponse( + error_message=str(exc), + error_type=ErrorSysResponse.ErrorType.REMOTE_CLEAN) + return ErrorSysResponse( error_message=(traceback.format_exc() - if self.receiver_returns_stack_traces else + if self.remote_errors_include_stack_traces else 'An internal error has occurred.'), - error_type=ErrorResponse.ErrorType.OTHER) + error_type=ErrorSysResponse.ErrorType.REMOTE) def _to_dict(self, message: Any, ids_by_type: dict[type, int], opname: str) -> dict: @@ -185,10 +181,10 @@ class MessageProtocol: assert isinstance(out, Message) return out - def response_from_dict(self, data: dict) -> Response: + def response_from_dict(self, data: dict) -> Response | SysResponse: """Decode a response from a json string.""" out = self._from_dict(data, self.response_types_by_id, 'response') - assert isinstance(out, Response) + assert isinstance(out, Response | SysResponse) return out # Weeeird; we get mypy errors returning dict[int, type] but @@ -234,7 +230,7 @@ class MessageProtocol: rsptypes.append(Response) for rsp_tp in rsptypes: # Skip these as they don't actually show up in code. - if rsp_tp is EmptyResponse or rsp_tp is ErrorResponse: + if rsp_tp is EmptySysResponse or rsp_tp is ErrorSysResponse: continue if (single_message_type and part == 'sender' and rsp_tp is not Response): @@ -341,10 +337,8 @@ class MessageProtocol: f'class {ppre}Bound{basename}(BoundMessageSender):\n' f' """Protocol-specific bound sender."""\n') - def _filt_tp_name(rtype: type[Response]) -> str: - # We accept None to equal EmptyResponse so reflect that - # in the type annotation. - return 'None' if rtype is EmptyResponse else rtype.__name__ + def _filt_tp_name(rtype: type[Response] | None) -> str: + return 'None' if rtype is None else rtype.__name__ # Define handler() overloads for all registered message types. if msgtypes: @@ -381,6 +375,7 @@ class MessageProtocol: for msgtype in msgtypes: msgtypevar = msgtype.__name__ + # rtypes = msgtype.get_response_types() rtypes = msgtype.get_response_types() if len(rtypes) > 1: rtypevar = ' | '.join( @@ -438,10 +433,8 @@ class MessageProtocol: # Define handler() overloads for all registered message types. - def _filt_tp_name(rtype: type[Response]) -> str: - # We accept None to equal EmptyResponse so reflect that - # in the type annotation. - return 'None' if rtype is EmptyResponse else rtype.__name__ + def _filt_tp_name(rtype: type[Response] | None) -> str: + return 'None' if rtype is None else rtype.__name__ if msgtypes: cbgn = 'Awaitable[' if is_async else '' diff --git a/dist/ba_data/python/efro/message/_receiver.py b/dist/ba_data/python/efro/message/_receiver.py index f45b255..5b23a80 100644 --- a/dist/ba_data/python/efro/message/_receiver.py +++ b/dist/ba_data/python/efro/message/_receiver.py @@ -11,13 +11,14 @@ import inspect import logging from typing import TYPE_CHECKING -from efro.message._message import (Message, Response, EmptyResponse, - ErrorResponse, UnregisteredMessageIDError) +from efro.message._message import (Message, Response, EmptySysResponse, + UnregisteredMessageIDError) if TYPE_CHECKING: from typing import Any, Callable, Awaitable from efro.message._protocol import MessageProtocol + from efro.message._message import SysResponse class MessageReceiver: @@ -53,7 +54,8 @@ class MessageReceiver: self._decode_filter_call: Callable[[Any, dict, Message], None] | None = None self._encode_filter_call: Callable[ - [Any, Message | None, Response, dict], None] | None = None + [Any, Message | None, Response | SysResponse, dict], + None] | None = None # TODO: don't currently have async encode equivalent # or either for sender; can add as needed. @@ -106,26 +108,27 @@ class MessageReceiver: assert issubclass(msgtype, Message) ret = anns.get('return') - responsetypes: tuple[type[Any] | type[None], ...] + responsetypes: tuple[type[Any] | None, ...] # Return types can be a single type or a union of types. if isinstance(ret, (_GenericAlias, types.UnionType)): targs = get_args(ret) - if not all(isinstance(a, type) for a in targs): + if not all(isinstance(a, (type, type(None))) for a in targs): raise TypeError(f'expected only types for "return" annotation;' f' got {targs}.') responsetypes = targs else: - if not isinstance(ret, type): + if not isinstance(ret, (type, type(None))): raise TypeError(f'expected one or more types for' f' "return" annotation; got a {type(ret)}.') # This seems like maybe a mypy bug. Appeared after adding # types.UnionType above. - responsetypes = (ret, ) # type: ignore + responsetypes = (ret, ) - # Return type of None translates to EmptyResponse. - responsetypes = tuple(EmptyResponse if r is type(None) else r - for r in responsetypes) # noqa + # This will contain NoneType for empty return cases, but + # we expect it to be None. + responsetypes = tuple(None if r is type(None) else r + for r in responsetypes) # Make sure our protocol has this message type registered and our # return types exactly match. (Technically we could return a subset @@ -178,7 +181,9 @@ class MessageReceiver: return call def encode_filter_method( - self, call: Callable[[Any, Message | None, Response, dict], None] + self, + call: Callable[[Any, Message | None, Response | SysResponse, dict], + None] ) -> Callable[[Any, Message | None, Response, dict], None]: """Function decorator for defining an encode filter. @@ -236,17 +241,21 @@ class MessageReceiver: response: Response | None) -> str: """Encode a response provided by the user for sending.""" - # A return value of None equals EmptyResponse. - if response is None: - response = EmptyResponse() - - assert isinstance(response, Response) + assert isinstance(response, Response | None) # (user should never explicitly return error-responses) - assert not isinstance(response, ErrorResponse) - assert type(response) in message.get_response_types() - response_dict = self.protocol.response_to_dict(response) + assert (response is None + or type(response) in message.get_response_types()) + + # A return value of None equals EmptySysResponse. + out_response: Response | SysResponse + if response is None: + out_response = EmptySysResponse() + else: + out_response = response + + response_dict = self.protocol.response_to_dict(out_response) if self._encode_filter_call is not None: - self._encode_filter_call(bound_obj, message, response, + self._encode_filter_call(bound_obj, message, out_response, response_dict) return self.protocol.encode_dict(response_dict) @@ -280,7 +289,7 @@ class MessageReceiver: if handler is None: raise RuntimeError(f'Got unhandled message type: {msgtype}.') response = handler(bound_obj, msg_decoded) - assert isinstance(response, (Response, type(None))) + assert isinstance(response, Response | None) return self.encode_user_response(bound_obj, msg_decoded, response) except Exception as exc: @@ -308,7 +317,7 @@ class MessageReceiver: if handler is None: raise RuntimeError(f'Got unhandled message type: {msgtype}.') response = await handler(bound_obj, msg_decoded) - assert isinstance(response, (Response, type(None))) + assert isinstance(response, Response | None) return self.encode_user_response(bound_obj, msg_decoded, response) except Exception as exc: diff --git a/dist/ba_data/python/efro/message/_sender.py b/dist/ba_data/python/efro/message/_sender.py index f2977ad..48a32e9 100644 --- a/dist/ba_data/python/efro/message/_sender.py +++ b/dist/ba_data/python/efro/message/_sender.py @@ -7,19 +7,17 @@ Supports static typing for message types and possible return types. from __future__ import annotations import logging -from typing import TYPE_CHECKING, TypeVar +from typing import TYPE_CHECKING from efro.error import CleanError, RemoteError, CommunicationError -from efro.message._message import EmptyResponse, ErrorResponse +from efro.message._message import EmptySysResponse, ErrorSysResponse, Response if TYPE_CHECKING: from typing import Any, Callable, Awaitable - from efro.message._message import Message, Response + from efro.message._message import Message, SysResponse from efro.message._protocol import MessageProtocol -TM = TypeVar('TM', bound='MessageSender') - class MessageSender: """Facilitates sending messages to a target and receiving responses. @@ -48,8 +46,8 @@ class MessageSender: [Any, str], Awaitable[str]] | None = None self._encode_filter_call: Callable[[Any, Message, dict], None] | None = None - self._decode_filter_call: Callable[[Any, Message, dict, Response], - None] | None = None + self._decode_filter_call: Callable[ + [Any, Message, dict, Response | SysResponse], None] | None = None def send_method( self, call: Callable[[Any, str], @@ -57,8 +55,9 @@ class MessageSender: """Function decorator for setting raw send method. Send methods take strings and should return strings. - Any Exception raised during the send_method manifests as - a CommunicationError for the message sender. + CommunicationErrors raised here will be returned to the sender + as such; all other exceptions will result in a RuntimeError for + the sender. """ assert self._send_raw_message_call is None self._send_raw_message_call = call @@ -70,8 +69,9 @@ class MessageSender: """Function decorator for setting raw send-async method. Send methods take strings and should return strings. - Any Exception raised during the send_method manifests as - a CommunicationError for the message sender. + CommunicationErrors raised here will be returned to the sender + as such; all other exceptions will result in a RuntimeError for + the sender. """ assert self._send_async_raw_message_call is None self._send_async_raw_message_call = call @@ -90,7 +90,8 @@ class MessageSender: return call def decode_filter_method( - self, call: Callable[[Any, Message, dict, Response], None] + self, call: Callable[[Any, Message, dict, Response | SysResponse], + None] ) -> Callable[[Any, Message, dict, Response], None]: """Function decorator for defining a decode filter. @@ -122,7 +123,8 @@ class MessageSender: ), ) - def send_split_part_1(self, bound_obj: Any, message: Message) -> Response: + def send_split_part_1(self, bound_obj: Any, + message: Message) -> Response | SysResponse: """Send a message synchronously. Generally you can just call send(); these split versions are @@ -139,15 +141,16 @@ class MessageSender: except Exception as exc: # Any error in the raw send call gets recorded as either # a local or communication error. - return ErrorResponse( - error_message=f'Error in send async method: {exc}', - error_type=(ErrorResponse.ErrorType.COMMUNICATION + return ErrorSysResponse( + error_message= + f'Error in MessageSender @send_method ({type(exc)}): {exc}', + error_type=(ErrorSysResponse.ErrorType.COMMUNICATION if isinstance(exc, CommunicationError) else - ErrorResponse.ErrorType.LOCAL)) + ErrorSysResponse.ErrorType.LOCAL)) return self._decode_raw_response(bound_obj, message, response_encoded) - async def send_split_part_1_async(self, bound_obj: Any, - message: Message) -> Response: + async def send_split_part_1_async( + self, bound_obj: Any, message: Message) -> Response | SysResponse: """Send a message asynchronously. Generally you can just call send(); these split versions are @@ -165,15 +168,18 @@ class MessageSender: except Exception as exc: # Any error in the raw send call gets recorded as either # a local or communication error. - return ErrorResponse( - error_message=f'Error in send async method: {exc}', - error_type=(ErrorResponse.ErrorType.COMMUNICATION + return ErrorSysResponse( + error_message= + f'Error in MessageSender @send_async_method ({type(exc)}):' + f' {exc}', + error_type=(ErrorSysResponse.ErrorType.COMMUNICATION if isinstance(exc, CommunicationError) else - ErrorResponse.ErrorType.LOCAL)) + ErrorSysResponse.ErrorType.LOCAL)) return self._decode_raw_response(bound_obj, message, response_encoded) - def send_split_part_2(self, message: Message, - raw_response: Response) -> Response | None: + def send_split_part_2( + self, message: Message, + raw_response: Response | SysResponse) -> Response | None: """Complete message sending (both sync and async). Generally you can just call send(); these split versions are @@ -193,7 +199,7 @@ class MessageSender: return self.protocol.encode_dict(msg_dict) def _decode_raw_response(self, bound_obj: Any, message: Message, - response_encoded: str) -> Response: + response_encoded: str) -> Response | SysResponse: """Create a Response from returned data. These Responses may encapsulate things like remote errors and @@ -201,6 +207,7 @@ class MessageSender: should be used to translate to special values like None or raise Exceptions. This function itself should never raise Exceptions. """ + response: Response | SysResponse try: response_dict = self.protocol.decode_dict(response_encoded) response = self.protocol.response_from_dict(response_dict) @@ -211,16 +218,17 @@ class MessageSender: # If we got to this point, we successfully communicated # with the other end so errors represent protocol mismatches # or other invalid data. For now let's just log it but perhaps - # we'd want to somehow embed it in the ErrorResponse to be + # we'd want to somehow embed it in the ErrorSysResponse to be # available directly to the user later. logging.exception('Error decoding raw response') - response = ErrorResponse( + response = ErrorSysResponse( error_message= 'Error decoding raw response; see log for details.', - error_type=ErrorResponse.ErrorType.LOCAL) + error_type=ErrorSysResponse.ErrorType.LOCAL) return response - def _unpack_raw_response(self, raw_response: Response) -> Response | None: + def _unpack_raw_response( + self, raw_response: Response | SysResponse) -> Response | None: """Given a raw Response, unpacks to special values or Exceptions. The result of this call is what should be passed to users. @@ -229,30 +237,31 @@ class MessageSender: run such that any raised Exception is active when the callback fires; not on the thread where the message was sent. """ - # EmptyResponse translates to None - if isinstance(raw_response, EmptyResponse): + # EmptySysResponse translates to None + if isinstance(raw_response, EmptySysResponse): return None # Some error occurred. Raise a local Exception for it. - if isinstance(raw_response, ErrorResponse): + if isinstance(raw_response, ErrorSysResponse): if (raw_response.error_type is - ErrorResponse.ErrorType.COMMUNICATION): + ErrorSysResponse.ErrorType.COMMUNICATION): raise CommunicationError(raw_response.error_message) - # If something went wrong on our end of the connection, + # If something went wrong on *our* end of the connection, # don't say it was a remote error. - if raw_response.error_type is ErrorResponse.ErrorType.LOCAL: + if raw_response.error_type is ErrorSysResponse.ErrorType.LOCAL: raise RuntimeError(raw_response.error_message) # If they want to support clean errors, do those. - if (self.protocol.preserve_clean_errors and - raw_response.error_type is ErrorResponse.ErrorType.CLEAN): + if (self.protocol.forward_clean_errors and raw_response.error_type + is ErrorSysResponse.ErrorType.REMOTE_CLEAN): raise CleanError(raw_response.error_message) - # In all other cases, just say something went wrong 'out there'. + # Everything else gets lumped in as a remote error. raise RemoteError(raw_response.error_message) + assert isinstance(raw_response, Response) return raw_response @@ -289,15 +298,16 @@ class BoundMessageSender: return await self._sender.send_async(bound_obj=self._obj, message=message) - async def send_split_part_1_async_untyped(self, - message: Message) -> Response: + async def send_split_part_1_async_untyped( + self, message: Message) -> Response | SysResponse: """Split send (part 1 of 2).""" assert self._obj is not None return await self._sender.send_split_part_1_async(bound_obj=self._obj, message=message) - def send_split_part_2_untyped(self, message: Message, - raw_response: Response) -> Response | None: + def send_split_part_2_untyped( + self, message: Message, + raw_response: Response | SysResponse) -> Response | None: """Split send (part 2 of 2).""" return self._sender.send_split_part_2(message=message, raw_response=raw_response) diff --git a/dist/ba_data/python/efro/rpc.py b/dist/ba_data/python/efro/rpc.py index a59d707..93c78ab 100644 --- a/dist/ba_data/python/efro/rpc.py +++ b/dist/ba_data/python/efro/rpc.py @@ -441,8 +441,9 @@ class RPCEndpoint: weakref.ref( asyncio.create_task( self._handle_raw_message(message_id=msgid, message=msg)))) - self._debug_print_call( - f'{self._label}: done handling message at {self._tm()}.') + if self._debug_print: + self._debug_print_call( + f'{self._label}: done handling message at {self._tm()}.') async def _handle_response_packet(self, big: bool) -> None: assert self._peer_info is not None @@ -558,7 +559,7 @@ class RPCEndpoint: # If that doesn't happen, make a fuss so we know to fix it. # The other end will simply never get a response to this # message. - logging.exception('Error handling message') + logging.exception('Error handling raw rpc message') return assert self._peer_info is not None diff --git a/dist/ba_data/python/efro/util.py b/dist/ba_data/python/efro/util.py index d9d3634..c8b8df0 100644 --- a/dist/ba_data/python/efro/util.py +++ b/dist/ba_data/python/efro/util.py @@ -27,11 +27,11 @@ if TYPE_CHECKING: from typing import Any, Callable, NoReturn T = TypeVar('T') -TVAL = TypeVar('TVAL') -TARG = TypeVar('TARG') -TSELF = TypeVar('TSELF') -TRET = TypeVar('TRET') -TENUM = TypeVar('TENUM', bound=Enum) +ValT = TypeVar('ValT') +ArgT = TypeVar('ArgT') +SelfT = TypeVar('SelfT') +RetT = TypeVar('RetT') +EnumT = TypeVar('EnumT', bound=Enum) class _EmptyObj: @@ -44,7 +44,7 @@ else: Call = functools.partial -def enum_by_value(cls: type[TENUM], value: Any) -> TENUM: +def enum_by_value(cls: type[EnumT], value: Any) -> EnumT: """Create an enum from a value. This is basically the same as doing 'obj = EnumType(value)' except @@ -251,15 +251,15 @@ class DirtyBit: return False -class DispatchMethodWrapper(Generic[TARG, TRET]): +class DispatchMethodWrapper(Generic[ArgT, RetT]): """Type-aware standin for the dispatch func returned by dispatchmethod.""" - def __call__(self, arg: TARG) -> TRET: + def __call__(self, arg: ArgT) -> RetT: pass @staticmethod def register( - func: Callable[[Any, Any], TRET]) -> Callable[[Any, Any], TRET]: + func: Callable[[Any, Any], RetT]) -> Callable[[Any, Any], RetT]: """Register a new dispatch handler for this dispatch-method.""" registry: dict[Any, Callable] @@ -267,8 +267,8 @@ class DispatchMethodWrapper(Generic[TARG, TRET]): # noinspection PyProtectedMember,PyTypeHints def dispatchmethod( - func: Callable[[Any, TARG], - TRET]) -> DispatchMethodWrapper[TARG, TRET]: + func: Callable[[Any, ArgT], + RetT]) -> DispatchMethodWrapper[ArgT, RetT]: """A variation of functools.singledispatch for methods. Note: as of Python 3.9 there is now functools.singledispatchmethod, @@ -307,7 +307,7 @@ def dispatchmethod( return cast(DispatchMethodWrapper, wrapper) -def valuedispatch(call: Callable[[TVAL], TRET]) -> ValueDispatcher[TVAL, TRET]: +def valuedispatch(call: Callable[[ValT], RetT]) -> ValueDispatcher[ValT, RetT]: """Decorator for functions to allow dispatching based on a value. This differs from functools.singledispatch in that it dispatches based @@ -318,21 +318,21 @@ def valuedispatch(call: Callable[[TVAL], TRET]) -> ValueDispatcher[TVAL, TRET]: return ValueDispatcher(call) -class ValueDispatcher(Generic[TVAL, TRET]): +class ValueDispatcher(Generic[ValT, RetT]): """Used by the valuedispatch decorator""" - def __init__(self, call: Callable[[TVAL], TRET]) -> None: + def __init__(self, call: Callable[[ValT], RetT]) -> None: self._base_call = call - self._handlers: dict[TVAL, Callable[[], TRET]] = {} + self._handlers: dict[ValT, Callable[[], RetT]] = {} - def __call__(self, value: TVAL) -> TRET: + def __call__(self, value: ValT) -> RetT: handler = self._handlers.get(value) if handler is not None: return handler() return self._base_call(value) - def _add_handler(self, value: TVAL, - call: Callable[[], TRET]) -> Callable[[], TRET]: + def _add_handler(self, value: ValT, + call: Callable[[], RetT]) -> Callable[[], RetT]: if value in self._handlers: raise RuntimeError(f'Duplicate handlers added for {value}') self._handlers[value] = call @@ -340,42 +340,42 @@ class ValueDispatcher(Generic[TVAL, TRET]): def register( self, - value: TVAL) -> Callable[[Callable[[], TRET]], Callable[[], TRET]]: + value: ValT) -> Callable[[Callable[[], RetT]], Callable[[], RetT]]: """Add a handler to the dispatcher.""" from functools import partial return partial(self._add_handler, value) def valuedispatch1arg( - call: Callable[[TVAL, TARG], - TRET]) -> ValueDispatcher1Arg[TVAL, TARG, TRET]: + call: Callable[[ValT, ArgT], + RetT]) -> ValueDispatcher1Arg[ValT, ArgT, RetT]: """Like valuedispatch but for functions taking an extra argument.""" return ValueDispatcher1Arg(call) -class ValueDispatcher1Arg(Generic[TVAL, TARG, TRET]): +class ValueDispatcher1Arg(Generic[ValT, ArgT, RetT]): """Used by the valuedispatch1arg decorator""" - def __init__(self, call: Callable[[TVAL, TARG], TRET]) -> None: + def __init__(self, call: Callable[[ValT, ArgT], RetT]) -> None: self._base_call = call - self._handlers: dict[TVAL, Callable[[TARG], TRET]] = {} + self._handlers: dict[ValT, Callable[[ArgT], RetT]] = {} - def __call__(self, value: TVAL, arg: TARG) -> TRET: + def __call__(self, value: ValT, arg: ArgT) -> RetT: handler = self._handlers.get(value) if handler is not None: return handler(arg) return self._base_call(value, arg) - def _add_handler(self, value: TVAL, - call: Callable[[TARG], TRET]) -> Callable[[TARG], TRET]: + def _add_handler(self, value: ValT, + call: Callable[[ArgT], RetT]) -> Callable[[ArgT], RetT]: if value in self._handlers: raise RuntimeError(f'Duplicate handlers added for {value}') self._handlers[value] = call return call def register( - self, value: TVAL - ) -> Callable[[Callable[[TARG], TRET]], Callable[[TARG], TRET]]: + self, value: ValT + ) -> Callable[[Callable[[ArgT], RetT]], Callable[[ArgT], RetT]]: """Add a handler to the dispatcher.""" from functools import partial return partial(self._add_handler, value) @@ -383,22 +383,22 @@ class ValueDispatcher1Arg(Generic[TVAL, TARG, TRET]): if TYPE_CHECKING: - class ValueDispatcherMethod(Generic[TVAL, TRET]): + class ValueDispatcherMethod(Generic[ValT, RetT]): """Used by the valuedispatchmethod decorator.""" - def __call__(self, value: TVAL) -> TRET: + def __call__(self, value: ValT) -> RetT: ... def register( - self, value: TVAL - ) -> Callable[[Callable[[TSELF], TRET]], Callable[[TSELF], TRET]]: + self, value: ValT + ) -> Callable[[Callable[[SelfT], RetT]], Callable[[SelfT], RetT]]: """Add a handler to the dispatcher.""" ... def valuedispatchmethod( - call: Callable[[TSELF, TVAL], - TRET]) -> ValueDispatcherMethod[TVAL, TRET]: + call: Callable[[SelfT, ValT], + RetT]) -> ValueDispatcherMethod[ValT, RetT]: """Like valuedispatch but works with methods instead of functions.""" # NOTE: It seems that to wrap a method with a decorator and have self @@ -407,18 +407,18 @@ def valuedispatchmethod( # in the function call dict and simply return a call. _base_call = call - _handlers: dict[TVAL, Callable[[TSELF], TRET]] = {} + _handlers: dict[ValT, Callable[[SelfT], RetT]] = {} - def _add_handler(value: TVAL, addcall: Callable[[TSELF], TRET]) -> None: + def _add_handler(value: ValT, addcall: Callable[[SelfT], RetT]) -> None: if value in _handlers: raise RuntimeError(f'Duplicate handlers added for {value}') _handlers[value] = addcall - def _register(value: TVAL) -> Callable[[Callable[[TSELF], TRET]], None]: + def _register(value: ValT) -> Callable[[Callable[[SelfT], RetT]], None]: from functools import partial return partial(_add_handler, value) - def _call_wrapper(self: TSELF, value: TVAL) -> TRET: + def _call_wrapper(self: SelfT, value: ValT) -> RetT: handler = _handlers.get(value) if handler is not None: return handler(self) @@ -433,7 +433,7 @@ def valuedispatchmethod( # In reality we just return a raw function call (for reasons listed above). # pylint: disable=undefined-variable, no-else-return if TYPE_CHECKING: - return ValueDispatcherMethod[TVAL, TRET]() + return ValueDispatcherMethod[ValT, RetT]() else: return _call_wrapper diff --git a/dist/ba_root/mods/changelogs.json b/dist/ba_root/mods/changelogs.json index 35d5b34..547b5fb 100644 --- a/dist/ba_root/mods/changelogs.json +++ b/dist/ba_root/mods/changelogs.json @@ -26,6 +26,14 @@ "69":{ "log":"added StumbledScoreScreen, End Vote System , in-game pop chat,error logging , other bug fix", "time":"1st June 2022" + }, + "70":{ + "log":"BS 1.7.5 update , more games and maps", + "time":"17 July 2022" + }, + "71":{ + "log":"BS 1.7.10 update , bug fix , V2 account support, prop rotation", + "time":"2 Oct 2022" } - -} \ No newline at end of file + +} diff --git a/dist/ba_root/mods/chatHandle/ChatCommands/Handlers.py b/dist/ba_root/mods/chatHandle/ChatCommands/Handlers.py index 04d7616..768ba78 100644 --- a/dist/ba_root/mods/chatHandle/ChatCommands/Handlers.py +++ b/dist/ba_root/mods/chatHandle/ChatCommands/Handlers.py @@ -1,7 +1,8 @@ # Released under the MIT License. See LICENSE for details. from playersData import pdata -import ba, _ba +import ba +import ba.internal @@ -17,7 +18,7 @@ def clientid_to_accountid(clientid): Returns: None """ - for i in _ba.get_game_roster(): + for i in ba.internal.get_game_roster(): if i['client_id'] == clientid: return i['account_id'] return None @@ -52,6 +53,6 @@ def check_permissions(accountid, command): def is_server(accid): - for i in _ba.get_game_roster(): + for i in ba.internal.get_game_roster(): if i['account_id']==accid and i['client_id']==-1: return True \ No newline at end of file diff --git a/dist/ba_root/mods/chatHandle/ChatCommands/Main.py b/dist/ba_root/mods/chatHandle/ChatCommands/Main.py index 38400b4..52aaeeb 100644 --- a/dist/ba_root/mods/chatHandle/ChatCommands/Main.py +++ b/dist/ba_root/mods/chatHandle/ChatCommands/Main.py @@ -11,6 +11,7 @@ from .Handlers import check_permissions from chatHandle.chatFilter import ChatFilter from bastd.actor import popuptext import ba, _ba +import ba.internal import setting from serverData import serverdata @@ -98,13 +99,13 @@ def QuickAccess(msg, client_id): if msg.startswith(","): name = "" teamid = 0 - for i in _ba.get_foreground_host_session().sessionplayers: + for i in ba.internal.get_foreground_host_session().sessionplayers: if i.inputdevice.client_id == client_id: teamid = i.sessionteam.id name = i.getname(True) - for i in _ba.get_foreground_host_session().sessionplayers: - if i.sessionteam and teamid == i.sessionteam.id and i.inputdevice.client_id != client_id: + for i in ba.internal.get_foreground_host_session().sessionplayers: + if hasattr(i, 'sessionteam') and i.sessionteam and teamid == i.sessionteam.id and i.inputdevice.client_id != client_id: _ba.screenmessage(name + ":" + msg[1:], clients=[i.inputdevice.client_id], color=(0.3, 0.6, 0.3), transient=True) @@ -117,7 +118,7 @@ def QuickAccess(msg, client_id): return None msgAr.insert(int(len(msgAr) / 2), "\n") for player in _ba.get_foreground_host_activity().players: - if player.sessionplayer.inputdevice.client_id == client_id: + if player.sessionplayer.inputdevice.client_id == client_id and player.actor.exists() and hasattr(player.actor.node,"position"): pos = player.actor.node.position with _ba.Context(_ba.get_foreground_host_activity()): popuptext.PopupText(" ".join(msgAr), (pos[0], pos[1] + 1, pos[2])).autoretain() diff --git a/dist/ba_root/mods/chatHandle/ChatCommands/commands/Fun.py b/dist/ba_root/mods/chatHandle/ChatCommands/commands/Fun.py index 45f04a7..13bf5e0 100644 --- a/dist/ba_root/mods/chatHandle/ChatCommands/commands/Fun.py +++ b/dist/ba_root/mods/chatHandle/ChatCommands/commands/Fun.py @@ -10,42 +10,42 @@ CommandAliases = ['inv', 'hl', 'creep', 'celeb', 'flo'] def ExcelCommand(command, arguments, clientid, accountid): """ - Checks The Command And Run Function - + Checks The Command And Run Function + Parameters: - command : str - arguments : str - clientid : int - accountid : int - + command : str + arguments : str + clientid : int + accountid : int + Returns: - None + None """ if command=='speed': speed(arguments) - + elif command == 'fly': fly(arguments) - + elif command in ['inv', 'invisible']: invi(arguments) - + elif command in ['hl', 'headless']: headless(arguments) - + elif command in ['creepy', 'creep']: creep(arguments) - + elif command in ['celebrate', 'celeb']: celeb(arguments) - + elif command == 'spaz': spaz(arguments) - + elif command in ['floater','flo']: floater(arguments,clientid) - + def floater(arguments,clientid): try: @@ -62,35 +62,35 @@ def speed(arguments): return else: corelib.set_speed(float(arguments[0])) - + def fly(arguments): - + if arguments == [] or arguments == ['']: return - - + + elif arguments[0] == 'all': - + activity = _ba.get_foreground_host_activity() - + for players in activity.players: if players.actor.node.fly != True: - players.actor.node.fly = True + players.actor.node.fly = True else: - players.actor.node.fly = False - + players.actor.node.fly = False + else: try: - + activity = _ba.get_foreground_host_activity() player = int(arguments[0]) - + if activity.players[player].actor.node.fly != True: - activity.players[player].actor.node.fly = True + activity.players[player].actor.node.fly = True else: - activity.players[player].actor.node.fly = False - + activity.players[player].actor.node.fly = False + except: return @@ -98,17 +98,17 @@ def fly(arguments): def invi(arguments): - + if arguments == [] or arguments == ['']: return - + elif arguments[0] == 'all': - + activity = _ba.get_foreground_host_activity() - + for i in activity.players: - body = i.actor.node - if body.torso_model != None: + if i.actor.exists() and i.actor.node.torso_model != None: + body = i.actor.node body.head_model = None body.torso_model = None body.upper_arm_model = None @@ -120,12 +120,12 @@ def invi(arguments): body.lower_leg_model = None body.style = 'cyborg' else: - + player = int(arguments[0]) activity = _ba.get_foreground_host_activity() - + body = activity.players[player].actor.node - + if body.torso_model != None: body.head_model = None body.torso_model = None @@ -142,28 +142,28 @@ def invi(arguments): def headless(arguments): - + if arguments == [] or arguments == ['']: return - + elif arguments[0] == 'all': - + activity = _ba.get_foreground_host_activity() - + for players in activity.players: - - node = players.actor.node + + node = players.actor.node if node.head_model != None: node.head_model = None node.style='cyborg' - + else: try: player = int(arguments[0]) activity = _ba.get_foreground_host_activity() - + node = activity.players[player].actor.node - + if node.head_model != None: node.head_model = None node.style='cyborg' @@ -173,29 +173,29 @@ def headless(arguments): def creep(arguments): - + if arguments == [] or arguments == ['']: return - + elif arguments[0] == 'all': - + activity = _ba.get_foreground_host_activity() - + for players in activity.players: - node = players.actor.node - + node = players.actor.node + if node.head_model != None: - node.head_model = None + node.head_model = None node.handlemessage(ba.PowerupMessage(poweruptype='punch')) node.handlemessage(ba.PowerupMessage(poweruptype='shield')) - + else: try: player = int(arguments[0]) activity = _ba.get_foreground_host_activity() - + node = activity.players[player].actor.node - + if node.head_model != None: node.head_model = None node.handlemessage(ba.PowerupMessage(poweruptype='punch')) @@ -206,27 +206,27 @@ def creep(arguments): def celeb(arguments): - + if arguments == [] or arguments == ['']: return - + elif arguments[0] == 'all': handlemsg_all(ba.CelebrateMessage()) - + else: try: player = int(arguments[0]) handlemsg(player, ba.CelebrateMessage()) except: return - + def spaz(arguments): - + if arguments == [] or arguments == ['']: return - + return diff --git a/dist/ba_root/mods/chatHandle/ChatCommands/commands/Handlers.py b/dist/ba_root/mods/chatHandle/ChatCommands/commands/Handlers.py index 33e6619..dfd8576 100644 --- a/dist/ba_root/mods/chatHandle/ChatCommands/commands/Handlers.py +++ b/dist/ba_root/mods/chatHandle/ChatCommands/commands/Handlers.py @@ -1,12 +1,13 @@ """ Some useful handlers to reduce lot of code """ import _ba, ba +import ba.internal def send(msg, clientid): """Shortcut To Send Private Msg To Client""" - - _ba.chatmessage(str(msg), clients=[clientid]) + + ba.internal.chatmessage(str(msg), clients=[clientid]) _ba.screenmessage(str(msg), transient=True, clients=[clientid]) @@ -15,12 +16,10 @@ def send(msg, clientid): def clientid_to_myself(clientid): """Return Player Index Of Self Player""" - - session = _ba.get_foreground_host_session() - - for i in range(len(session.sessionplayers)): - if session.sessionplayers[i].inputdevice.client_id == clientid: - return int(session.sessionplayers[i].id) + + for i in _ba.get_foreground_host_activity().players: + if i.sessionplayer.inputdevice.client_id == clientid: + return i @@ -28,7 +27,7 @@ def clientid_to_myself(clientid): def handlemsg(client, msg): """Handles Spaz Msg For Single Player""" - + activity = _ba.get_foreground_host_activity() activity.players[client].actor.node.handlemessage(msg) @@ -38,9 +37,9 @@ def handlemsg(client, msg): def handlemsg_all(msg): """Handle Spaz message for all players in activity""" - + activity = _ba.get_foreground_host_activity() - + for i in activity.players: i.actor.node.handlemessage(msg) diff --git a/dist/ba_root/mods/chatHandle/ChatCommands/commands/Management.py b/dist/ba_root/mods/chatHandle/ChatCommands/commands/Management.py index c2616c5..7c8ec34 100644 --- a/dist/ba_root/mods/chatHandle/ChatCommands/commands/Management.py +++ b/dist/ba_root/mods/chatHandle/ChatCommands/commands/Management.py @@ -3,6 +3,7 @@ from playersData import pdata # from tools.whitelist import add_to_white_list, add_commit_to_logs from serverData import serverdata import ba, _ba, time, setting +import ba.internal import _thread from tools import playlist Commands = ['lm', 'gp', 'party', 'quit', 'kickvote','maxplayers','playlist','ban','kick', 'remove', 'end', 'quit', 'mute', 'unmute', 'slowmo', 'nv', 'dv', 'pause', 'cameramode', 'createrole', 'addrole', 'removerole', 'addcommand', 'addcmd', 'removecommand','getroles', 'removecmd', 'changetag','customtag','customeffect','add', 'spectators', 'lobbytime'] @@ -110,13 +111,13 @@ def ExcelCommand(command, arguments, clientid, accountid): def changepartysize(arguments): if len(arguments)==0: - _ba.chatmessage("enter number") + ba.internal.chatmessage("enter number") else: - _ba.set_public_party_max_size(int(arguments[0])) + ba.internal.set_public_party_max_size(int(arguments[0])) def changeplaylist(arguments): if len(arguments)==0: - _ba.chatmessage("enter list code or name") + ba.internal.chatmessage("enter list code or name") else: if arguments[0]=='coop': serverdata.coopmode=True @@ -127,7 +128,7 @@ def changeplaylist(arguments): def kick(arguments): - _ba.disconnect_client(int(arguments[0])) + ba.internal.disconnect_client(int(arguments[0])) return def kikvote(arguments, clientid): if arguments == [] or arguments == [''] or len(arguments) < 2: @@ -139,7 +140,7 @@ def kikvote(arguments, clientid): else: try: cl_id=int(arguments[1]) - for ros in _ba.get_game_roster(): + for ros in ba.internal.get_game_roster(): if ros["client_id"]==cl_id: if ros["account_id"] in serverdata.clients: serverdata.clients[ros["account_id"]]["canStartKickVote"]=True @@ -154,7 +155,7 @@ def kikvote(arguments, clientid): else: try: cl_id=int(arguments[1]) - for ros in _ba.get_game_roster(): + for ros in ba.internal.get_game_roster(): if ros["client_id"]==cl_id: _ba.disable_kickvote(ros["account_id"]) send("Kick-vote disabled for this person", clientid) @@ -167,14 +168,14 @@ def kikvote(arguments, clientid): return def last_msgs(clientid): - for i in _ba.get_chat_messages(): + for i in ba.internal.get_chat_messages(): send(i,clientid) def get_profiles(arguments,clientid): try: playerID = int(arguments[0]) num = 1 - for i in _ba.get_foreground_host_session().sessionplayers[playerID].inputdevice.get_player_profiles(): + for i in ba.internal.get_foreground_host_session().sessionplayers[playerID].inputdevice.get_player_profiles(): try: send(f"{num})- {i}",clientid) num += 1 @@ -185,11 +186,11 @@ def get_profiles(arguments,clientid): def party_toggle(arguments): if arguments == ['public']: - _ba.set_public_party_enabled(True) - _ba.chatmessage("party is public now") + ba.internal.set_public_party_enabled(True) + ba.internal.chatmessage("party is public now") elif arguments == ['private']: - _ba.set_public_party_enabled(False) - _ba.chatmessage("party is private now") + ba.internal.set_public_party_enabled(False) + ba.internal.chatmessage("party is private now") else: pass @@ -206,7 +207,7 @@ def ban(arguments): try: cl_id=int(arguments[0]) ac_id="" - for ros in _ba.get_game_roster(): + for ros in ba.internal.get_game_roster(): if ros["client_id"]==cl_id: _thread.start_new_thread(pdata.ban_player,(ros['account_id'],)) @@ -232,7 +233,7 @@ def mute(arguments): try: cl_id=int(arguments[0]) ac_id="" - for ros in _ba.get_game_roster(): + for ros in ba.internal.get_game_roster(): if ros["client_id"]==cl_id: _thread.start_new_thread(pdata.mute,(ros['account_id'],)) @@ -251,7 +252,7 @@ def un_mute(arguments): try: cl_id=int(arguments[0]) ac_id="" - for ros in _ba.get_game_roster(): + for ros in ba.internal.get_game_roster(): if ros["client_id"]==cl_id: pdata.unmute(ros['account_id']) ac_id=ros['account_id'] @@ -269,13 +270,13 @@ def remove(arguments): return elif arguments[0] == 'all': - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() for i in session.sessionplayers: i.remove_from_game() else: try: - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() for i in session.sessionplayers: if i.inputdevice.client_id== int(arguments[0]): i.remove_from_game() @@ -369,7 +370,7 @@ def create_role(arguments): def add_role_to_player(arguments): try: - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() for i in session.sessionplayers: if i.inputdevice.client_id== int(arguments[1]): roles=pdata.add_player_role(arguments[0],i.get_v1_account_id()) @@ -380,7 +381,7 @@ def add_role_to_player(arguments): def remove_role_from_player(arguments): try: - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() for i in session.sessionplayers: if i.inputdevice.client_id== int(arguments[1]): roles=pdata.remove_player_role(arguments[0],i.get_v1_account_id()) @@ -389,7 +390,7 @@ def remove_role_from_player(arguments): return def get_roles_of_player(arguments,clientid): try: - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() roles=[] reply="" for i in session.sessionplayers: @@ -409,7 +410,7 @@ def change_role_tag(arguments): def set_custom_tag(arguments): try: - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() for i in session.sessionplayers: if i.inputdevice.client_id== int(arguments[1]): roles=pdata.set_tag(arguments[0],i.get_v1_account_id()) @@ -417,7 +418,7 @@ def set_custom_tag(arguments): return def set_custom_effect(arguments): try: - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() for i in session.sessionplayers: if i.inputdevice.client_id== int(arguments[1]): roles=pdata.set_effect(arguments[0],i.get_v1_account_id()) @@ -453,11 +454,11 @@ def remove_command_to_role(arguments): # if arguments[0] == 'on': # if settings["white_list"]["whitelist_on"]: -# _ba.chatmessage("Already on") +# ba.internal.chatmessage("Already on") # else: # settings["white_list"]["whitelist_on"] = True # setting.commit(settings) -# _ba.chatmessage("whitelist on") +# ba.internal.chatmessage("whitelist on") # from tools import whitelist # whitelist.Whitelist() # return @@ -465,16 +466,16 @@ def remove_command_to_role(arguments): # elif arguments[0] == 'off': # settings["white_list"]["whitelist_on"] = False # setting.commit(settings) -# _ba.chatmessage("whitelist off") +# ba.internal.chatmessage("whitelist off") # return # else: - # rost = _ba.get_game_roster() + # rost = ba.internal.get_game_roster() # for i in rost: # if i['client_id'] == int(arguments[0]): # add_to_white_list(i['account_id'], i['display_string']) - # _ba.chatmessage(str(i['display_string'])+" whitelisted") + # ba.internal.chatmessage(str(i['display_string'])+" whitelisted") # add_commit_to_logs(accountid+" added "+i['account_id']) @@ -488,22 +489,23 @@ def spectators(arguments): if arguments[0] == 'on': settings["white_list"]["spectators"] = True setting.commit(settings) - _ba.chatmessage("spectators on") + ba.internal.chatmessage("spectators on") elif arguments[0] == 'off': settings["white_list"]["spectators"] = False setting.commit(settings) - _ba.chatmessage("spectators off") + ba.internal.chatmessage("spectators off") def change_lobby_check_time(arguments): - try: - argument = int(arguments[0]) - except: - _ba.chatmessage("must type numbe to change lobby check time") - settings = setting.get_settings_data() - settings["white_list"]["lobbychecktime"] = argument - setting.commit(settings) - _ba.chatmessage(f"lobby check time is {arg} now") + try: + argument = int(arguments[0]) + except: + ba.internal.chatmessage("must type number to change lobby check time") + return + settings = setting.get_settings_data() + settings["white_list"]["lobbychecktime"] = argument + setting.commit(settings) + ba.internal.chatmessage(f"lobby check time is {argument} now") diff --git a/dist/ba_root/mods/chatHandle/ChatCommands/commands/NormalCommands.py b/dist/ba_root/mods/chatHandle/ChatCommands/commands/NormalCommands.py index 180be9b..d564349 100644 --- a/dist/ba_root/mods/chatHandle/ChatCommands/commands/NormalCommands.py +++ b/dist/ba_root/mods/chatHandle/ChatCommands/commands/NormalCommands.py @@ -1,5 +1,6 @@ from .Handlers import send import ba, _ba +import ba.internal from stats import mystats from ba._general import Call import _thread @@ -56,7 +57,7 @@ def list(clientid): list = p.format('Name', 'Client ID' , 'Player ID')+seprator - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() for index, player in enumerate(session.sessionplayers): @@ -76,7 +77,7 @@ def accountid_request(arguments, clientid, accountid): else: try: - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() player = session.sessionplayers[int(arguments[0])] name = player.getname(full=True, icon=True) diff --git a/dist/ba_root/mods/chatHandle/chatFilter/ChatFilter.py b/dist/ba_root/mods/chatHandle/chatFilter/ChatFilter.py index 78d40c5..8d4bef2 100644 --- a/dist/ba_root/mods/chatHandle/chatFilter/ChatFilter.py +++ b/dist/ba_root/mods/chatHandle/chatFilter/ChatFilter.py @@ -1,5 +1,6 @@ # Released under the MIT License. See LICENSE for details. import ba, _ba +import ba.internal from serverData import serverdata from features import profanity from tools import servercheck @@ -52,7 +53,7 @@ def filter(msg,pb_id,client_id): smsgcount+=1 if smsgcount>=3: logger.log(pb_id+" | kicked for chat spam") - _ba.disconnect_client(client_id) + ba.internal.disconnect_client(client_id) smsgcount=0 _ba.screenmessage("Don\'t SPAM!", color=(1,0,0), transient=True, clients=[client_id]) if not check_permissions(pb_id): @@ -88,7 +89,7 @@ def addWarn(pb_id,client_id): if warn > settings["maxWarnCount"]: _ba.screenmessage(settings["afterWarnKickMsg"],color=(1,0,0),transient=True,clients=[client_id]) logger.log(pb_id+" | kicked for chat spam") - _ba.disconnect_client(client_id) + ba.internal.disconnect_client(client_id) _thread.start_new_thread(servercheck.reportSpam,(pb_id,)) else: diff --git a/dist/ba_root/mods/chatHandle/handlechat.py b/dist/ba_root/mods/chatHandle/handlechat.py index d8d8829..ba3a7b2 100644 --- a/dist/ba_root/mods/chatHandle/handlechat.py +++ b/dist/ba_root/mods/chatHandle/handlechat.py @@ -7,6 +7,7 @@ from tools import logger, servercheck from chatHandle.chatFilter import ChatFilter from features import EndVote import ba, _ba +import ba.internal import setting settings = setting.get_settings_data() @@ -22,7 +23,7 @@ def filter_chat_message(msg, client_id): displaystring = "" currentname = "" - for i in _ba.get_game_roster(): + for i in ba.internal.get_game_roster(): if i['client_id'] == client_id: acid = i['account_id'] try: diff --git a/dist/ba_root/mods/custom_hooks.py b/dist/ba_root/mods/custom_hooks.py index d06a40c..b62ee20 100644 --- a/dist/ba_root/mods/custom_hooks.py +++ b/dist/ba_root/mods/custom_hooks.py @@ -18,7 +18,7 @@ import time import os import ba import _ba - +import logging from ba import _hooks from bastd.activity import dualteamscore, multiteamscore, drawscore from bastd.activity.coopscore import CoopScoreScreen @@ -53,6 +53,17 @@ class modSetup(ba.Plugin): if settings["afk_remover"]['enable']: afk_check.checkIdle().start() + if(settings["useV2Account"]): + from tools import account + if(ba.internal.get_v1_account_state()=='signed_in' and ba.internal.get_v1_account_type()=='V2'): + logging.debug("Account V2 is active") + else: + logging.warning("Account V2 login require ....stay tuned.") + ba.timer(3, ba.Call(logging.debug,"Starting Account V2 login process....")) + ba.timer(6,account.AccountUtil) + else: + ba.app.accounts_v2.set_primary_credentials(None) + ba.internal.sign_in_v1('Local') ba.timer(60,playlist.flush_playlists) def on_app_shutdown(self): pass @@ -72,7 +83,7 @@ def playerspaz_init(playerspaz: ba.Player, node: ba.Node, player: ba.Player): def bootstraping(): """Bootstarps the server.""" - print("Bootstraping mods..") + logging.warning("Bootstraping mods...") # server related _ba.set_server_device_name(settings["HostDeviceName"]) _ba.set_server_name(settings["HostName"]) diff --git a/dist/ba_root/mods/features/EndVote.py b/dist/ba_root/mods/features/EndVote.py index dee5af8..90332b5 100644 --- a/dist/ba_root/mods/features/EndVote.py +++ b/dist/ba_root/mods/features/EndVote.py @@ -1,6 +1,7 @@ # EndVote by -mr.smoothy import _ba, ba +import ba.internal import time last_end_vote_start_time = 0 @@ -23,11 +24,11 @@ def vote_end(pb_id, client_id): clients=[client_id]) return if len(voters) == 0: - _ba.chatmessage("end vote started") + ba.internal.chatmessage("end vote started") # clean up voters list active_players = [] - for player in _ba.get_game_roster(): + for player in ba.internal.get_game_roster(): active_players.append(player['account_id']) for voter in voters: if voter not in active_players: @@ -39,10 +40,10 @@ def vote_end(pb_id, client_id): update_vote_text(required_votes(len(active_players)) - len(voters)) if required_votes(len(active_players)) - len( voters) == 3: # lets dont spam chat/screen message with votes required , only give message when only 3 votes left - _ba.chatmessage("3 more end votes required") + ba.internal.chatmessage("3 more end votes required") if len(voters) >= required_votes(len(active_players)): - _ba.chatmessage("end vote succeed") + ba.internal.chatmessage("end vote succeed") try: with _ba.Context(_ba.get_foreground_host_activity()): _ba.get_foreground_host_activity().end_game() diff --git a/dist/ba_root/mods/features/afk_check.py b/dist/ba_root/mods/features/afk_check.py index ad64e9d..5d35251 100644 --- a/dist/ba_root/mods/features/afk_check.py +++ b/dist/ba_root/mods/features/afk_check.py @@ -3,6 +3,7 @@ import time import ba from ba._general import Call import _ba +import ba.internal import setting settings = setting.get_settings_data() INGAME_TIME=settings["afk_remover"]["ingame_idle_time_in_secs"] @@ -14,7 +15,7 @@ class checkIdle(object): self.lobbies={} def check(self): current=ba.time(ba.TimeType.REAL,timeformat=ba.TimeFormat.MILLISECONDS) - for player in _ba.get_foreground_host_session().sessionplayers: + for player in ba.internal.get_foreground_host_session().sessionplayers: last_input=int(player.inputdevice.get_last_input_time()) afk_time=int((current-last_input)/1000) if afk_time in range(INGAME_TIME,INGAME_TIME+20): @@ -23,7 +24,7 @@ class checkIdle(object): player.remove_from_game() if LOBBY_KICK: current_players=[] - for player in _ba.get_game_roster(): + for player in ba.internal.get_game_roster(): if player['client_id'] !=-1 and len(player['players']) ==0: current_players.append(player['client_id']) if player['client_id'] not in self.lobbies: @@ -32,14 +33,14 @@ class checkIdle(object): if lobby_afk in range(INLOBBY_TIME,INLOBBY_TIME+10): _ba.screenmessage("Join game within "+str(INLOBBY_TIME+10-lobby_afk)+" secs",color=(1,0,0),transient=True,clients=[player['client_id']]) if lobby_afk > INLOBBY_TIME+ 10: - _ba.disconnect_client(player['client_id'],0) + ba.internal.disconnect_client(player['client_id'],0) # clean the lobbies dict temp=self.lobbies.copy() for clid in temp: if clid not in current_players: del self.lobbies[clid] def warn_player(self,pbid,msg): - for player in _ba.get_game_roster(): + for player in ba.internal.get_game_roster(): if player["account_id"]==pbid: _ba.screenmessage(msg,color=(1,0,0),transient=True,clients=[player['client_id']]) diff --git a/dist/ba_root/mods/features/discord_bot.py b/dist/ba_root/mods/features/discord_bot.py index 9364f5e..651ed3c 100644 --- a/dist/ba_root/mods/features/discord_bot.py +++ b/dist/ba_root/mods/features/discord_bot.py @@ -6,6 +6,7 @@ from discord.ext.commands import Bot import ba from ba._general import Call import _ba +import ba.internal import json import os import _thread @@ -47,7 +48,7 @@ async def on_message(message): channel=message.channel if message.channel.id==logsChannelID: - _ba.pushcall(Call(_ba.chatmessage,message.content),from_other_thread=True) + _ba.pushcall(Call(ba.internal.chatmessage,message.content),from_other_thread=True) @client.event @@ -168,15 +169,15 @@ class BsDataThread(object): currentMap='' global stats - for i in _ba.get_game_roster(): + for i in ba.internal.get_game_roster(): try: liveplayers[i['account_id']]={'name':i['players'][0]['name_full'],'client_id':i['client_id'],'device_id':i['display_string']} except: liveplayers[i['account_id']]={'name':"",'clientid':i['client_id'],'device_id':i['display_string']} try: - nextMap=_ba.get_foreground_host_session().get_next_game_description().evaluate() + nextMap=ba.internal.get_foreground_host_session().get_next_game_description().evaluate() - current_game_spec=_ba.get_foreground_host_session()._current_game_spec + current_game_spec=ba.internal.get_foreground_host_session()._current_game_spec gametype: Type[GameActivity] =current_game_spec['resolved_type'] currentMap=gametype.get_settings_display_string(current_game_spec).evaluate() @@ -187,7 +188,7 @@ class BsDataThread(object): #system={'cpu':80,'ram':34} # stats['system']=system stats['roster']=liveplayers - stats['chats']=_ba.get_chat_messages() + stats['chats']=ba.internal.get_chat_messages() stats['playlist']=minigame diff --git a/dist/ba_root/mods/features/team_balancer.py b/dist/ba_root/mods/features/team_balancer.py index ff2c335..efaa787 100644 --- a/dist/ba_root/mods/features/team_balancer.py +++ b/dist/ba_root/mods/features/team_balancer.py @@ -1,4 +1,5 @@ -import _ba,ba +import _ba,ba +import ba.internal import setting from serverData import serverdata @@ -9,7 +10,7 @@ from tools import playlist def balanceTeams(): - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() if settings["coopModeWithLessPlayers"]["enable"] and len(session.sessionplayers) < settings["coopModeWithLessPlayers"]["minPlayerToExitCoop"]: playlist.setPlaylist('coop') return @@ -37,7 +38,7 @@ def movePlayers(fromTeam,toTeam,count): return # disabling team balance for now , until we found solution # Error : on score screen when shifted player left the game on_player_leave unable to found player in activity team - session=_ba.get_foreground_host_session() + session=ba.internal.get_foreground_host_session() fromTeam=session.sessionteams[fromTeam] toTeam=session.sessionteams[toTeam] for i in range(0,count): @@ -50,12 +51,12 @@ def movePlayers(fromTeam,toTeam,count): toTeam.players.append(player) def broadCastShiftMsg(pb_id): - for ros in _ba.get_game_roster(): + for ros in ba.internal.get_game_roster(): if ros['account_id']==pb_id: _ba.screenmessage("Shifted "+ros["display_string"]+" to balance team") def on_player_join(): - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() if len(session.sessionplayers)>1: return if isinstance(session,DualTeamSession): @@ -69,7 +70,7 @@ def on_player_join(): def checkToExitCoop(): - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() if len(session.sessionplayers) >= settings["coopModeWithLessPlayers"]["minPlayerToExitCoop"] and not serverdata.coopmode: playlist.setPlaylist('default') diff --git a/dist/ba_root/mods/features/text_on_map.py b/dist/ba_root/mods/features/text_on_map.py index 7a31106..3b8512d 100644 --- a/dist/ba_root/mods/features/text_on_map.py +++ b/dist/ba_root/mods/features/text_on_map.py @@ -4,6 +4,7 @@ from ba._generated.enums import TimeType import ba, _ba +import ba.internal import setting from stats import mystats from datetime import datetime @@ -23,7 +24,7 @@ class textonmap: nextMap="" try: - nextMap=_ba.get_foreground_host_session().get_next_game_description().evaluate() + nextMap=ba.internal.get_foreground_host_session().get_next_game_description().evaluate() except: pass self.index = 0 diff --git a/dist/ba_root/mods/maps/BridgitMash.so b/dist/ba_root/mods/maps/BridgitMash.so index 8488aeb..eeae93d 100644 Binary files a/dist/ba_root/mods/maps/BridgitMash.so and b/dist/ba_root/mods/maps/BridgitMash.so differ diff --git a/dist/ba_root/mods/maps/BridgitParallelo.so b/dist/ba_root/mods/maps/BridgitParallelo.so index 7714fa3..fe6a0f6 100644 Binary files a/dist/ba_root/mods/maps/BridgitParallelo.so and b/dist/ba_root/mods/maps/BridgitParallelo.so differ diff --git a/dist/ba_root/mods/maps/WoodenFloor.py b/dist/ba_root/mods/maps/WoodenFloor.py index ad31dd4..27229af 100644 --- a/dist/ba_root/mods/maps/WoodenFloor.py +++ b/dist/ba_root/mods/maps/WoodenFloor.py @@ -5,13 +5,41 @@ from typing import TYPE_CHECKING import ba,_ba from bastd.gameutils import SharedObjects from bastd.actor.playerspaz import PlayerSpaz +import copy if TYPE_CHECKING: from typing import Any, List, Dict - +class mapdefs: + points = {} + # noinspection PyDictCreation + boxes = {} + boxes['area_of_interest_bounds'] = (0.0, 1.185751251, 0.4326226188) + ( + 0.0, 0.0, 0.0) + (29.8180273, 11.57249038, 18.89134176) + boxes['edge_box'] = (-0.103873591, 0.4133341891, 0.4294651013) + ( + 0.0, 0.0, 0.0) + (22.48295719, 1.290242794, 8.990252454) + points['ffa_spawn1'] = (-0.08015551329, 0.02275111462, + -4.373674593) + (8.895057015, 1.0, 0.444350722) + points['ffa_spawn2'] = (-0.08015551329, 0.02275111462, + 4.076288941) + (8.895057015, 1.0, 0.444350722) + points['flag1'] = (-10.99027878, 0.05744967453, 0.1095578275) + points['flag2'] = (11.01486398, 0.03986567039, 0.1095578275) + points['flag_default'] = (-0.1001374046, 0.04180340146, 0.1095578275) + boxes['goal1'] = (12.22454533, 1.0, + 0.1087926362) + (0.0, 0.0, 0.0) + (2.0, 2.0, 12.97466313) + boxes['goal2'] = (-12.15961605, 1.0, + 0.1097860203) + (0.0, 0.0, 0.0) + (2.0, 2.0, 13.11856424) + boxes['map_bounds'] = (0.0, 1.185751251, 0.4326226188) + (0.0, 0.0, 0.0) + ( + 42.09506485, 22.81173179, 29.76723155) + points['powerup_spawn1'] = (5.414681236, 0.9515026107, -5.037912441) + points['powerup_spawn2'] = (-5.555402285, 0.9515026107, -5.037912441) + points['powerup_spawn3'] = (5.414681236, 0.9515026107, 5.148223181) + points['powerup_spawn4'] = (-5.737266365, 0.9515026107, 5.148223181) + points['spawn1'] = (-10.03866341, 0.02275111462, 0.0) + (0.5, 1.0, 4.0) + points['spawn2'] = (9.823107149, 0.01092306765, 0.0) + (0.5, 1.0, 4.0) + points['tnt1'] = (-0.08421587483, 0.9515026107, -0.7762602271) class WoodenFloor(ba.Map): """Stadium map for football games.""" - from bastd.mapdata import football_stadium as defs + defs = mapdefs defs.points['spawn1'] = (-12.03866341, 0.02275111462, 0.0) + (0.5, 1.0, 4.0) defs.points['spawn2'] = (12.823107149, 0.01092306765, 0.0) + (0.5, 1.0, 4.0) name = 'Wooden Floor' @@ -28,7 +56,7 @@ class WoodenFloor(ba.Map): @classmethod def on_preload(cls) -> Any: data: dict[str, Any] = { - + 'model_bg': ba.getmodel('doomShroomBG'), 'bg_vr_fill_model': ba.getmodel('natureBackgroundVRFill'), 'collide_model': ba.getcollidemodel('bridgitLevelCollide'), @@ -79,20 +107,19 @@ class WoodenFloor(ba.Map): xpos = (point.x - box_position[0]) / box_scale[0] zpos = (point.z - box_position[2]) / box_scale[2] return xpos < -0.5 or xpos > 0.5 or zpos < -0.5 or zpos > 0.5 - - + def _handle_player_collide(self): try: player = ba.getcollision().opposingnode.getdelegate( PlayerSpaz, True) except ba.NotFoundError: return - - + + if player.is_alive(): player.shatter(True) - -ba._map.register_map(WoodenFloor) \ No newline at end of file + +ba._map.register_map(WoodenFloor) diff --git a/dist/ba_root/mods/playersData/pdata.py b/dist/ba_root/mods/playersData/pdata.py index ad1cd4b..a146183 100644 --- a/dist/ba_root/mods/playersData/pdata.py +++ b/dist/ba_root/mods/playersData/pdata.py @@ -12,7 +12,9 @@ import _thread from serverData import serverdata from tools.file_handle import OpenJson -import _ba # pylint: disable=import-error +# pylint: disable=import-error +import _ba +import ba.internal import json if TYPE_CHECKING: @@ -131,7 +133,7 @@ def add_profile( serverdata.clients[account_id]["rejoincount"] = 1 serverdata.clients[account_id]["lastJoin"] = time.time() cid = 113 - for ros in _ba.get_game_roster(): + for ros in ba.internal.get_game_roster(): if ros['account_id'] == account_id: cid = ros['client_id'] serverdata.clients[account_id]["lastIP"] = _ba.get_client_ip(cid) diff --git a/dist/ba_root/mods/plugins/CharacterChooser.py b/dist/ba_root/mods/plugins/CharacterChooser.py index aa6d8ab..0546ad0 100644 --- a/dist/ba_root/mods/plugins/CharacterChooser.py +++ b/dist/ba_root/mods/plugins/CharacterChooser.py @@ -35,6 +35,7 @@ from __future__ import annotations from typing import TYPE_CHECKING import ba,_ba +import ba.internal from bastd.actor.playerspaz import PlayerSpaz @@ -163,7 +164,7 @@ def _set_ready(self, ready: bool) -> None: # Give their input-device UI ownership too # (prevent someone else from snatching it in crowded games) - _ba.set_ui_input_device(self._sessionplayer.inputdevice) + ba.internal.set_ui_input_device(self._sessionplayer.inputdevice) return if ready==False: diff --git a/dist/ba_root/mods/plugins/bcs_plugin.py b/dist/ba_root/mods/plugins/bcs_plugin.py index 8271cce..cc5b240 100644 --- a/dist/ba_root/mods/plugins/bcs_plugin.py +++ b/dist/ba_root/mods/plugins/bcs_plugin.py @@ -5,6 +5,7 @@ from typing import Optional, Any, Dict, List, Type, Sequence from ba._gameactivity import GameActivity import ba,_ba +import ba.internal import json import os import _thread @@ -53,7 +54,7 @@ class BsDataThread(object): nextMap='' currentMap='' global stats - for i in _ba.get_game_roster(): + for i in ba.internal.get_game_roster(): try: @@ -61,9 +62,9 @@ class BsDataThread(object): except: liveplayers[i['account_id']]={'name':"",'clientid':i['client_id'],'device_id':i['display_string']} try: - nextMap=_ba.get_foreground_host_session().get_next_game_description().evaluate() + nextMap=ba.internal.get_foreground_host_session().get_next_game_description().evaluate() - current_game_spec=_ba.get_foreground_host_session()._current_game_spec + current_game_spec=ba.internal.get_foreground_host_session()._current_game_spec gametype: Type[GameActivity] =current_game_spec['resolved_type'] currentMap=gametype.get_settings_display_string(current_game_spec).evaluate() @@ -74,7 +75,7 @@ class BsDataThread(object): system={'cpu':"null",'ram':'null'} stats['system']=system stats['roster']=liveplayers - stats['chats']=_ba.get_chat_messages() + stats['chats']=ba.internal.get_chat_messages() stats['playlist']=minigame stats['teamInfo']=self.getTeamInfo() @@ -83,7 +84,7 @@ class BsDataThread(object): def getTeamInfo(self): data={} - session=_ba.get_foreground_host_session() + session=ba.internal.get_foreground_host_session() data['sessionType']=type(session).__name__ teams=session.sessionteams for team in teams: diff --git a/dist/ba_root/mods/plugins/elPatronPowerups.py b/dist/ba_root/mods/plugins/elPatronPowerups.py index 24d6801..c3e5245 100644 --- a/dist/ba_root/mods/plugins/elPatronPowerups.py +++ b/dist/ba_root/mods/plugins/elPatronPowerups.py @@ -1,10 +1,2352 @@ # ba_meta require api 7 from __future__ import annotations from typing import TYPE_CHECKING -import base64 _sp_ = ('\n') -exec(base64.b64decode("CmltcG9ydCBiYSxfYmEscmFuZG9tLHRpbWUsZGF0ZXRpbWUsd2Vha3JlZixqc29uLG9zCmZyb20gYmFzdGQudWkucHJvZmlsZSBpbXBvcnQgYnJvd3Nlcgpmcm9tIGJhc3RkLmFjdG9yIGltcG9ydCBib21iCmZyb20gYmFzdGQuYWN0b3IgaW1wb3J0IHBvd2VydXBib3ggIGFzIHB1cGJveApmcm9tIGJhc3RkLmFjdG9yLnNwYXpib3QgaW1wb3J0IFNwYXpCb3QKZnJvbSBiYXN0ZC5hY3Rvci5ib21iIGltcG9ydCAoQm9tYixCbGFzdCkKZnJvbSBiYXN0ZC51aS5wb3B1cCBpbXBvcnQgKFBvcHVwV2luZG93LFBvcHVwTWVudVdpbmRvdyxQb3B1cE1lbnUpCmZyb20gYmFzdGQuYWN0b3Iuc3BheiBpbXBvcnQgKFNwYXosU3BhekZhY3RvcnksUGlja3VwTWVzc2FnZSwgUHVuY2hIaXRNZXNzYWdlLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBDdXJzZUV4cGxvZGVNZXNzYWdlLCBCb21iRGllZE1lc3NhZ2UpCmZyb20gYmFzdGQubWFpbm1lbnUgaW1wb3J0IChNYWluTWVudUFjdGl2aXR5LE1haW5NZW51U2Vzc2lvbikKZnJvbSBiYXN0ZC5nYW1ldXRpbHMgaW1wb3J0IFNoYXJlZE9iamVjdHMKZnJvbSBiYXN0ZC5hY3Rvci5wb3dlcnVwYm94IGltcG9ydCBQb3dlcnVwQm94RmFjdG9yeQpmcm9tIGJhc3RkLmFjdG9yLnBvcHVwdGV4dCBpbXBvcnQgUG9wdXBUZXh0CmZyb20gYmFzdGQudWkuY29uZmlybSBpbXBvcnQgQ29uZmlybVdpbmRvdwpmcm9tIGJhc3RkLmFjdG9yLnNwYXogaW1wb3J0ICoKCmlmIFRZUEVfQ0hFQ0tJTkc6CiAgICBwYXNzCgojID09PSBNb2QgbWFkZSBieSBAUGF0cm9uX01vZHogPT09CgpkZWYgZ2V0bGFuZ3VhZ2UodGV4dCwgc3Viczogc3RyID0gTm9uZSwgYWxtYWNlbjogbGlzdCA9IFtdKToKICAgIGlmIGFsbWFjZW4gPT0gW106IGFsbWFjZW4gPSBsaXN0KHJhbmdlKDEwMDApKQogICAgbGFuZyA9IF9iYS5hcHAubGFuZy5sYW5ndWFnZQogICAgdHJhbnNsYXRlID0geyJSZXNldCI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiUmVpbmljaWFyIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJSZXNldCIsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiAiUmVpbmljaWFyIn0sCiAgICAgICAgICAgICAgICAgIk5vdGhpbmciOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogIlNpbiBwb3RlbmNpYWRvcmVzIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJObyBwb3dlcnVwcyIsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiAiU2VtIHBvd2VydXBzIn0sCiAgICAgICAgICAgICAgICAgIkFjdGlvbiAxIjoKICAgICAgICAgICAgICAgICAgICAgIHsiU3BhbmlzaCI6ICJQb3RlbmNpYWRvcmVzIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJQb3dlcnVwcyIsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiAiUG93ZXJ1cHMifSwKICAgICAgICAgICAgICAgICAiQWN0aW9uIDIiOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogIkNvbmZpZ3VyYWNpw4PCs24iLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIlNldHRpbmdzIiwKICAgICAgICAgICAgICAgICAgICAgICAiUG9ydHVndWVzZSI6ICJEZWZpbmnDg8Knw4PCtWVzIn0sCiAgICAgICAgICAgICAgICAgIkFjdGlvbiAzIjoKICAgICAgICAgICAgICAgICAgICAgIHsiU3BhbmlzaCI6ICJFeHRyYXMiLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIkV4dHJhcyIsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiAiRXh0cmFzIn0sCiAgICAgICAgICAgICAgICAgIkFjdGlvbiA0IjoKICAgICAgICAgICAgICAgICAgICAgIHsiU3BhbmlzaCI6ICJUaWVuZGEiLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIlN0b3JlIiwKICAgICAgICAgICAgICAgICAgICAgICAiUG9ydHVndWVzZSI6ICJMb2phIn0sCiAgICAgICAgICAgICAgICAgIkFjdGlvbiA1IjoKICAgICAgICAgICAgICAgICAgICAgIHsiU3BhbmlzaCI6ICJDYW5qZWFyIGPDg8KzZGlnbyIsCiAgICAgICAgICAgICAgICAgICAgICAgIkVuZ2xpc2giOiAiRW50ZXIgQ29kZSIsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiAiQ8ODwrNkaWdvIHByb21vY2lvbmFsIn0sCiAgICAgICAgICAgICAgICAgIkN1c3RvbSI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJDdXN0b21pemUiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogIkN1c3RvbWl6YXIifSwKICAgICAgICAgICAgICAgICAiSW1wYWlybWVudCBCb21icyI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiQm9tYmFzIG1lbm9zY2FibyIsCiAgICAgICAgICAgICAgICAgICAgICAgIkVuZ2xpc2giOiAiSHlwZXJhY3RpdmUgYm9tYnMiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogIkJvbWJhcyBoaXBlcmF0aXZhcyJ9LAogICAgICAgICAgICAgICAgICJTcGVlZCI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiVmVsb2NpZGFkIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJTcGVlZCIsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiAiVmVsb2NpZGFkZSJ9LAogICAgICAgICAgICAgICAgICJGaXJlIEJvbWJzIjoKICAgICAgICAgICAgICAgICAgICAgIHsiU3BhbmlzaCI6ICJCb21iYXMgZGUgZnVlZ28iLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIkZpcmUgQm9tYnMiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogIkJvbWJhcyBkZSBmb2dvIn0sCiAgICAgICAgICAgICAgICAgIkljZSBNYW4iOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogIkhvbWJyZSBkZSBoaWVsbyIsCiAgICAgICAgICAgICAgICAgICAgICAgIkVuZ2xpc2giOiAiSWNlIG1hbiIsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiAiSG9tZW0gZGUgZ2VsbyJ9LAogICAgICAgICAgICAgICAgICJGbHkgQm9tYnMiOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogIkJvbWJhcyBleHBhbnNpdmFzIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJFeHBhbnNpdmUgYm9tYnMiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogIkJvbWJhcyBleHBhbnNpdmFzIn0sCiAgICAgICAgICAgICAgICAgIkdvb2RieWUiOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogIsOCwqFIYXN0YSBsdWVnbyEiLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIkdvb2RieWUhIiwKICAgICAgICAgICAgICAgICAgICAgICAiUG9ydHVndWVzZSI6ICJBZGV1cyEifSwKICAgICAgICAgICAgICAgICAiSGVhbGluZyBEYW1hZ2UiOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogIkF1dG8tY3VyYWNpw4PCs24iLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIkhlYWxpbmcgRGFtYWdlIiwKICAgICAgICAgICAgICAgICAgICAgICAiUG9ydHVndWVzZSI6ICJBdXRvLWN1cmEifSwKICAgICAgICAgICAgICAgICAiVGFuayBTaGllbGQiOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogIlPDg8K6cGVyIGJsaW5kYWplIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJSZWluZm9yY2VkIHNoaWVsZCIsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiAiRXNjdWRvIHJlZm9yw4PCp2FkbyJ9LAogICAgICAgICAgICAgICAgICJUYW5rIFNoaWVsZCBQVEciOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogIlBvcmNlbnRhamUgZGUgZGlzbWludWNpw4PCs24iLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIlBlcmNlbnRhZ2UgZGVjcmVhc2VkIiwKICAgICAgICAgICAgICAgICAgICAgICAiUG9ydHVndWVzZSI6ICJQZXJjZW50dWFsIHJlZHV6aWRvIn0sCiAgICAgICAgICAgICAgICAgIkhlYWxpbmcgRGFtYWdlIFBURyI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiUG9yY2VudGFqZSBkZSByZWN1cGVyYWNpw4PCs24gZGUgc2FsdWQiLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIlBlcmNlbnRhZ2Ugb2YgaGVhbHRoIHJlY292ZXJlZCIsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiAiUG9yY2VudGFnZW0gZGUgcmVjdXBlcmHDg8Knw4PCo28gZGUgc2HDg8K6ZGUifSwKICAgICAgICAgICAgICAgICAiU1k6IEJBTEwiOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogIkVzZmVyYSIsCiAgICAgICAgICAgICAgICAgICAgICAgIkVuZ2xpc2giOiAiU3BoZXJlIiwKICAgICAgICAgICAgICAgICAgICAgICAiUG9ydHVndWVzZSI6ICJFc2ZlcmEifSwKICAgICAgICAgICAgICAgICAiU1k6IEltcGFjdCI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiRXNwZWNpYWwiLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIlNwZWNpYWwiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogIkVzcGVjaWFsIn0sCiAgICAgICAgICAgICAgICAgIlNZOiBFZ2ciOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogIkh1ZXZpdG8iLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIkVnZyBzaGFwZSIsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiAiT3ZvIn0sCiAgICAgICAgICAgICAgICAgIlBvd2VydXAgU2NhbGUiOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogIlRhbWHDg8KxbyBkZWwgcG90ZW5jaWFkb3IiLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIlBvd2VydXBzIHNpemUiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogIlRhbWFuaG8gZGUgcG93ZXJ1cHMifSwKICAgICAgICAgICAgICAgICAiUG93ZXJ1cCBXaXRoIFNoaWVsZCI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiUG90ZW5jaWFkb3JlcyBjb24gZXNjdWRvIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJQb3dlcnVwcyB3aXRoIHNoaWVsZCIsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiAiUG93ZXJ1cHMgY29tIGVzY3VkbyJ9LAogICAgICAgICAgICAgICAgICJQb3dlcnVwIFRpbWUiOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogIk1vc3RyYXIgVGVtcG9yaXphZG9yIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJTaG93IGVuZCB0aW1lIiwKICAgICAgICAgICAgICAgICAgICAgICAiUG9ydHVndWVzZSI6ICJNb3N0cmFyIGNyb27Dg8K0bWV0cm8ifSwKICAgICAgICAgICAgICAgICAiUG93ZXJ1cCBTdHlsZSI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiRm9ybWEgZGUgbG9zIHBvdGVuY2lhZG9yZXMiLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIlNoYXBlIG9mIHBvd2VydXAiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogIkZvcm1hIGRlIHBvd2VydXAifSwKICAgICAgICAgICAgICAgICAiUG93ZXJ1cCBOYW1lIjoKICAgICAgICAgICAgICAgICAgICAgIHsiU3BhbmlzaCI6ICJNb3N0cmFyIG5vbWJyZSBlbiBsb3MgcG90ZW5jaWFkb3JlcyIsCiAgICAgICAgICAgICAgICAgICAgICAgIkVuZ2xpc2giOiAiU2hvdyBuYW1lIG9uIHBvd2VydXBzIiwKICAgICAgICAgICAgICAgICAgICAgICAiUG9ydHVndWVzZSI6ICJNb3N0cmFyIG5vbWUgZW0gcG93ZXJ1cHMifSwKICAgICAgICAgICAgICAgICAiUGVyY2VudGFnZSI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiUHJvYmFiaWxpZGFkIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJTaG93IHBlcmNlbnRhZ2UiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogIk1vc3RyYXIgcG9yY2VudGFnZW0ifSwKICAgICAgICAgICAgICAgICAiT25seSBJdGVtcyI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiU8ODwrNsbyBBY2Nlc29yaW9zIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJPbmx5IHV0ZW5zaWxzIiwKICAgICAgICAgICAgICAgICAgICAgICAiUG9ydHVndWVzZSI6ICJBcGVuYXMgdXRlbnNpbGlvcyJ9LAogICAgICAgICAgICAgICAgICJOZXciOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogIk51ZXZvIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJOZXciLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogIk5vdm8ifSwKICAgICAgICAgICAgICAgICAiT25seSBCb21icyI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiU8ODwrNsbyBCb21iYXMiLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIk9ubHkgYm9tYnMiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogIkFwZW5hcyBib21iYXMifSwKICAgICAgICAgICAgICAgICAiQ29pbnMgMCI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiTW9uZWRhcyBJbnN1ZmljaWVudGVzIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJJbnN1ZmZpY2llbnQgY29pbnMiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogIk1vZWRhcyBpbnN1ZmljaWVudGVzIn0sCiAgICAgICAgICAgICAgICAgIlB1cmNoYXNlIjoKICAgICAgICAgICAgICAgICAgICAgIHsiU3BhbmlzaCI6ICJDb21wcmEgcmVhbGl6YWRhIGNvcnJlY3RhbWVudGUiLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIlN1Y2Nlc3NmdWwgcHVyY2hhc2UiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogIkNvbXByYSBCZW0gU3VjZWRpZGEifSwKICAgICAgICAgICAgICAgICAiRG91YmxlIFByb2R1Y3QiOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogIllhIGhhcyBjb21wcmFkbyBlc3RlIGFydMODwq1jdWxvIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJZb3UndmUgYWxyZWFkeSBib3VnaHQgdGhpcyIsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiAiVm9jZSBqYSBjb21wcm91IGlzdG8ifSwKICAgICAgICAgICAgICAgICAiQm91Z2h0IjoKICAgICAgICAgICAgICAgICAgICAgIHsiU3BhbmlzaCI6ICJDb21wcmFkbyIsCiAgICAgICAgICAgICAgICAgICAgICAgIkVuZ2xpc2giOiAiQm91Z2h0IiwKICAgICAgICAgICAgICAgICAgICAgICAiUG9ydHVndWVzZSI6ICJDb21wcm91In0sCiAgICAgICAgICAgICAgICAgIkNvbmZpcm0gUHVyY2hhc2UiOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogZidUaWVuZXMge3N1YnN9IG1vbmVkYXMuIHtfc3BffSDDgsK/RGVzZWFzIGNvbXByYXIgZXN0bz8nLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogZidZb3UgaGF2ZSB7c3Vic30gY29pbnMuIHtfc3BffSBEbyB5b3Ugd2FudCB0byBidXkgdGhpcz8nLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogZidWb2PDg8KqIHRlbSB7c3Vic30gbW9lZGFzLiB7X3NwX30gRGVzZWphIGNvbXByYXIgaXN0bz8nfSwKICAgICAgICAgICAgICAgICAiRmlyZUJvbWJzIFN0b3JlIjoKICAgICAgICAgICAgICAgICAgICAgIHsiU3BhbmlzaCI6ICdCb21iYXMgZGUgZnVlZ28nLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogJ0ZpcmUgYm9tYnMnLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogJ0JvbWJhcyBkZSBpbmPDg8KqbmRpbyd9LAogICAgICAgICAgICAgICAgICJUaW1lciBTdG9yZSI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAnVGVtcG9yaXphZG9yJywKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICdUaW1lcicsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiAnVGltZXInfSwKICAgICAgICAgICAgICAgICAiUGVyY2VudGFnZXMgU3RvcmUiOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogJ0V4dHJhcycsCiAgICAgICAgICAgICAgICAgICAgICAgIkVuZ2xpc2giOiAnRXh0cmFzJywKICAgICAgICAgICAgICAgICAgICAgICAiUG9ydHVndWVzZSI6ICdFeHRyYXMnfSwKICAgICAgICAgICAgICAgICAiQmxvY2sgT3B0aW9uIFN0b3JlIjoKICAgICAgICAgICAgICAgICAgICAgIHsiU3BhbmlzaCI6IGYiVXV1cHMuLntfc3BffUVzdGEgb3BjacODwrNuIGVzdMODwqEgYmxvcXVlYWRhLntfc3BffSBQYXJhIGFjY2VkZXIgYSBlbGxhIHB1ZWRlcyB7X3NwX30gY29tcHJhcmxhIGVuIGxhIHRpZW5kYS57X3NwX30gR3JhY2lhcy4uLiIsCiAgICAgICAgICAgICAgICAgICAgICAgIkVuZ2xpc2giOiBmIk9vb29wcy4uLntfc3BffVRoaXMgb3B0aW9uIGlzIGJsb2NrZWQuIHtfc3BffSBUbyBhY2Nlc3MgaXQgeW91IGNhbiBidXkge19zcF99IGl0IGluIHRoZSBzdG9yZS57X3NwX30gVGhhbmsgeW91Li4uIiwKICAgICAgICAgICAgICAgICAgICAgICAiUG9ydHVndWVzZSI6IGYiT29vcHMuLi57X3NwX31Fc3RhIG9ww4PCp8ODwqNvIGVzdMODwqEgYmxvcXVlYWRhLiB7X3NwX30gUGFyYSBhY2Vzc8ODwqEtbG8sIHZvY8ODwqogcG9kZSB7X3NwX30gY29tcHLDg8KhLWxvIG5hIGxvamEue19zcF99IE9icmlnYWRvLi4uIn0sCiAgICAgICAgICAgICAgICAgIlRydWUgQ29kZSI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiw4LCoUPDg8KzZGlnbyBjYW5qZWFkbyEiLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIlN1Y2Nlc3NmdWwgY29kZSEiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogIsOCwqFDw4PCs2RpZ28gdsODwqFsaWRvISJ9LAogICAgICAgICAgICAgICAgICJGYWxzZSBDb2RlIjoKICAgICAgICAgICAgICAgICAgICAgIHsiU3BhbmlzaCI6ICJDw4PCs2RpZ28geWEgY2FuamVhZG8iLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIkV4cGlyZWQgY29kZSIsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiAiQ8ODwrNkaWdvIGV4cGlyYWRvIn0sCiAgICAgICAgICAgICAgICAgIkludmFsaWQgQ29kZSI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiQ8ODwrNkaWdvIGludsODwqFsaWRvIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6ICJJbnZhbGlkIGNvZGUiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogIkPDg8KzZGlnbyBpbnbDg8KhbGlkbyJ9LAogICAgICAgICAgICAgICAgICJSZXdhcmQgQ29kZSI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiBmIsOCwqFGZWxpY2l0YWNpb25lcyEgw4LCoUdhbmFzdGUge3N1YnN9IG1vbmVkYXMhIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6IGYiQ29uZ3JhdHVsYXRpb25zISBZb3UndmUge3N1YnN9IGNvaW5zIiwKICAgICAgICAgICAgICAgICAgICAgICAiUG9ydHVndWVzZSI6IGYiUGFyYWLDg8KpbnMhIFZvY8ODwqogdGVtIHtzdWJzfSBtb2VkYXMifSwKICAgICAgICAgICAgICAgICAiQ3JlYXRvciI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiAiTW9kIGNyZWFkbyBwb3IgQFBhdHLDg8Kzbk1vZHoiLAogICAgICAgICAgICAgICAgICAgICAgICJFbmdsaXNoIjogIk1vZCBjcmVhdGVkIGJ5IEBQYXRyw4PCs25Nb2R6IiwKICAgICAgICAgICAgICAgICAgICAgICAiUG9ydHVndWVzZSI6ICJNb2QgY3JlYWRvIGJ5IEBQYXRyw4PCs25Nb2R6In0sCiAgICAgICAgICAgICAgICAgIk1vZCBJbmZvIjoKICAgICAgICAgICAgICAgICAgICAgIHsiU3BhbmlzaCI6IGYiVW4gbW9kIGdlbmlhbCBxdWUgdGUgcGVybWl0ZSBnZXN0aW9uYXIge19zcF99IGxvcyBwb3RlbmNpYWRvcmVzIGEgdHUgYW50b2pvLiB7X3NwX30gdGFtYmnDg8KpbiBpbmNsdXllIDggcG90ZW5jaWFkb3JlcyBleHRyYXtfc3BffSBkZWphbmRvIDE3IGVuIHRvdGFsLi4uIMOCwqFHdWF5ISIsCiAgICAgICAgICAgICAgICAgICAgICAgIkVuZ2xpc2giOiBmIkEgY29vbCBtb2QgdGhhdCBhbGxvd3MgeW91IHRvIG1hbmFnZSB7X3NwX30gcG93ZXJ1cHMgYXQgeW91ciB3aGltcy4ge19zcF99IGFsc28gaW5jbHVkZXMgOCBleHRyYSBwb3dlcnVwc3tfc3BffSBsZWF2aW5nIDE3IGluIHRvdGFsLi4uIFdvdyEiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogZiJVbSBtb2QgbGVnYWwgcXVlIHBlcm1pdGUgcXVlIHZvY8ODwqogZ2VyZW5jaWUgb3N7X3NwX30gcG93ZXJ1cHMgZGUgZGUgYWNvcmRvIGNvbSBzZXVzIGNhcHJpY2hvcy4ge19zcF99IHRhbWLDg8KpbSBpbmNsdWkgOCBwb3dlcnVwcyBleHRyYXMse19zcF99IGRlaXhhbmRvIDE3IG5vIHRvdGFsLi4uIFVhdSEifSwKICAgICAgICAgICAgICAgICAiQ29pbnMgTWVzc2FnZSI6CiAgICAgICAgICAgICAgICAgICAgICB7IlNwYW5pc2giOiBmIlJlY29tcGVuc2E6IHtzdWJzfSBNb25lZGFzIiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6IGYiUmV3YXJkOiB7c3Vic30gQ29pbnMiLAogICAgICAgICAgICAgICAgICAgICAgICJQb3J0dWd1ZXNlIjogZiJSZWNvbXBlbnNhOiB7c3Vic30gTW9lZGFzIn0sCiAgICAgICAgICAgICAgICAgIkNvaW5zIExpbWl0IE1lc3NhZ2UiOgogICAgICAgICAgICAgICAgICAgICAgeyJTcGFuaXNoIjogZiJHYW5hc3RlIHthbG1hY2VuWzBdfSBNb25lZGFzLntfc3BffSBQZXJvIGhhcyBzdXBlcmFkbyBlbCBsw4PCrW1pdGUgZGUge2FsbWFjZW5bMV19IiwKICAgICAgICAgICAgICAgICAgICAgICAiRW5nbGlzaCI6IGYiWW91IHdvbiB7YWxtYWNlblswXX0gQ29pbnMuIHtfc3BffSBCdXQgeW91IGhhdmUgZXhjZWVkZWQgdGhlIGxpbWl0IG9mIHthbG1hY2VuWzFdfSIsCiAgICAgICAgICAgICAgICAgICAgICAgIlBvcnR1Z3Vlc2UiOiBmIlZvY8ODwqogZ2FuaG91IHthbG1hY2VuWzBdfSBNb2VkYXMuIHtfc3BffSBNYXMgdm9jw4PCqiBleGNlZGV1IG8gbGltaXRlIGRlIHthbG1hY2VuWzFdfSJ9LAogICAgICAgICAgICAgICAgIH0KICAgIGxhbmd1YWdlcyA9IFsnU3BhbmlzaCcsJ1BvcnR1Z3Vlc2UnLCdFbmdsaXNoJ10KICAgIGlmIGxhbmcgbm90IGluIGxhbmd1YWdlczogbGFuZyA9ICdFbmdsaXNoJwoKICAgIGlmIHRleHQgbm90IGluIHRyYW5zbGF0ZToKICAgICAgICByZXR1cm4gdGV4dAogICAgCiAgICByZXR1cm4gdHJhbnNsYXRlW3RleHRdW2xhbmddCgppbXBvcnQgc2V0dGluZwpzZXR0aW5ncz1zZXR0aW5nLmdldF9zZXR0aW5nc19kYXRhKCkKCmRlZiBzZXR0aW5nc19kaXN0cmlidXRpb24oKToKICAgIHJldHVybiBzZXR0aW5nc1siZWxQYXRyb25Qb3dlcnVwcyJdWyJzZXR0aW5ncyJdCgoKCmFwZyA9IGJhLmFwcC5jb25maWcKCmFwZ1snUFBNIFNldHRpbmdzJ10gPSBzZXR0aW5nc19kaXN0cmlidXRpb24oKQoKCmNvbmZpZyA9IGFwZ1snUFBNIFNldHRpbmdzJ10KCmRlZiBkZWZhdWx0X3Bvd2VydXBzKCk6CiAgICByZXR1cm4gc2V0dGluZ3NbImVsUGF0cm9uUG93ZXJ1cHMiXVsiUXVhbnRpdHkiXQoKCmNvbmZpZ1snUG93ZXJ1cHMnXSA9IGRlZmF1bHRfcG93ZXJ1cHMoKQoKCnBvd2VydXBzID0gY29uZmlnWydQb3dlcnVwcyddCgojID09PSBFWFRSQVMgPT09CgpHTE9CQUwgPSB7IlRhYiI6ICdBY3Rpb24gMScsCiAgICAgICAgICAiQ2xzIFBvd2VydXAiOiAwLAogICAgICAgICAgIkNvaW5zIE1lc3NhZ2UiOiBbXX0KCiMgPT09IFNUT1JFID09PQpkZWYgcHJvbW9fY29kZXMoKToKICAgIHJldHVybiB7IkctQW01NGlnTzQyT3MiOiBbVHJ1ZSwxMTAwXSwKICAgICAgICAgICAgIlAtdFJvOG5NOGRaIjogW1RydWUsMjgwMF0sCiAgICAgICAgICAgICJZLXRVMkIzUyI6IFtUcnVlLDUwMF0sCiAgICAgICAgICAgICJCLTBtQjNSWVQyeiI6IFtUcnVlLDkxMF0sCiAgICAgICAgICAgICJCLUFzZDE0bU9OOUcwRCI6IFtUcnVlLDkxMF0sCiAgICAgICAgICAgICJELXJBY0swY0oyMyI6IFtUcnVlLDkxMF0sCiAgICAgICAgICAgICJFLWEyN1pPNmYzWSI6IFtUcnVlLDYwMF0sCiAgICAgICAgICAgICJFLUFtNTRpZ080Mk9zIjogW1RydWUsNjAwXSwKICAgICAgICAgICAgIkUtTTR1TjNLMzRYQiI6IFtUcnVlLDg0MF0sCiAgICAgICAgICAgICJQTS03MzFDbGNBRiI6IFtUcnVlLDUwMDAwXX0KICAgICAgICAgICAgCmRlZiBzdG9yZV9pdGVtcygpOgogICAgcmV0dXJuIHsiQnV5IEZpcmVib21icyI6IEZhbHNlLAogICAgICAgICAgICAiQnV5IE9wdGlvbiI6IEZhbHNlLAogICAgICAgICAgICAiQnV5IFBlcmNlbnRhZ2UiOiBGYWxzZX0KCmlmIGFwZy5nZXQoJ0JlYXIgQ29pbicpIGlzIE5vbmU6CiAgICBhcGdbJ0JlYXIgQ29pbiddID0gMAogICAgYXBnLmFwcGx5X2FuZF9jb21taXQoKQogICAgCmlmIGFwZy5nZXQoJ0JlYXIgQ29pbicpIGlzIG5vdCBOb25lOgogICAgaWYgYXBnWydCZWFyIENvaW4nXSA8PSAwOgogICAgICAgIGFwZ1snQmVhciBDb2luJ10gPSAwCiAgICBhcGdbJ0JlYXIgQ29pbiddID0gaW50KGFwZ1snQmVhciBDb2luJ10pCgppZiBhcGcuZ2V0KCdCZWFyIFN0b3JlJykgaXMgTm9uZToKICAgIGFwZ1snQmVhciBTdG9yZSddID0ge30KICAgIApmb3IgaSxqIGluIHN0b3JlX2l0ZW1zKCkuaXRlbXMoKToKICAgIHN0b3JlICA9IGFwZ1snQmVhciBTdG9yZSddCiAgICBpZiBpIG5vdCBpbiBzdG9yZToKICAgICAgICBpZiBzdG9yZS5nZXQoaSkgaXMgTm9uZToKICAgICAgICAgICAgc3RvcmVbaV0gPSBqCiAgICBhcGcuYXBwbHlfYW5kX2NvbW1pdCgpCgpTVE9SRSA9IGFwZ1snQmVhciBTdG9yZSddCgppZiBTVE9SRS5nZXQoJ1Byb21vIENvZGUnKSBpcyBOb25lOgogICAgU1RPUkVbJ1Byb21vIENvZGUnXSA9IHByb21vX2NvZGVzKCkKCmZvciBpLHggaW4gcHJvbW9fY29kZXMoKS5pdGVtcygpOgogICAgcG1jb2RlID0gU1RPUkVbJ1Byb21vIENvZGUnXQogICAgaWYgaSBub3QgaW4gcG1jb2RlOgogICAgICAgIGlmIHBtY29kZS5nZXQoaSkgaXMgTm9uZToKICAgICAgICAgICAgcG1jb2RlW2ldID0geAoKYXBnLmFwcGx5X2FuZF9jb21taXQoKQoKY2xhc3MgQmVhclN0b3JlOgogICAgZGVmIF9faW5pdF9fKHNlbGYsCiAgICAgICAgICAgICAgICAgcHJpY2U6IGludCA9IDEwMDAsCiAgICAgICAgICAgICAgICAgdmFsdWU6IHN0ciA9ICcnLAogICAgICAgICAgICAgICAgIGNhbGxiYWNrOiBjYWxsID0gTm9uZSk6CiAgICAgICAgICAgICAgICAgICAgIAogICAgICAgIHNlbGYucHJpY2UgPSBwcmljZQogICAgICAgIHNlbGYudmFsdWUgPSB2YWx1ZQogICAgICAgIHNlbGYuc3RvcmUgPSBTVE9SRVt2YWx1ZV0KICAgICAgICBzZWxmLmNvaW5zID0gYXBnWydCZWFyIENvaW4nXQogICAgICAgIHNlbGYuY2FsbGJhY2sgPSBjYWxsYmFjawogICAgICAgICAgICAgICAgIAogICAgZGVmIGJ1eShzZWxmKToKICAgICAgICBpZiBub3Qgc2VsZi5zdG9yZToKICAgICAgICAgICAgaWYgc2VsZi5jb2lucyA+PSAoc2VsZi5wcmljZSk6CiAgICAgICAgICAgICAgICBkZWYgY29uZmlybSgpOgogICAgICAgICAgICAgICAgICAgIFNUT1JFW3NlbGYudmFsdWVdID0gVHJ1ZQogICAgICAgICAgICAgICAgICAgIGFwZ1snQmVhciBDb2luJ10gLT0gaW50KHNlbGYucHJpY2UpCiAgICAgICAgICAgICAgICAgICAgYmEuc2NyZWVubWVzc2FnZShnZXRsYW5ndWFnZSgnUHVyY2hhc2UnKSwoMCwxLDApKQogICAgICAgICAgICAgICAgICAgIGJhLnBsYXlzb3VuZChiYS5nZXRzb3VuZCgnY2FzaFJlZ2lzdGVyJykpCiAgICAgICAgICAgICAgICAgICAgYXBnLmFwcGx5X2FuZF9jb21taXQoKQogICAgICAgICAgICAgICAgICAgIHNlbGYuY2FsbGJhY2soKQogICAgICAgICAgICAgICAgQ29uZmlybVdpbmRvdyhnZXRsYW5ndWFnZSgnQ29uZmlybSBQdXJjaGFzZScsc3Vicz1zZWxmLmNvaW5zKSwKICAgICAgICAgICAgICAgICAgICAgIHdpZHRoPTQwMCwgaGVpZ2h0PTEyMCwgYWN0aW9uPWNvbmZpcm0sIG9rX3RleHQ9YmEuTHN0cihyZXNvdXJjZT0nb2tUZXh0JykpCiAgICAgICAgICAgIGVsc2U6CiAgICAgICAgICAgICAgICBiYS5zY3JlZW5tZXNzYWdlKGdldGxhbmd1YWdlKCdDb2lucyAwJyksKDEsMCwwKSkKICAgICAgICAgICAgICAgIGJhLnBsYXlzb3VuZChiYS5nZXRzb3VuZCgnZXJyb3InKSkKICAgICAgICBlbHNlOgogICAgICAgICAgICBiYS5zY3JlZW5tZXNzYWdlKGdldGxhbmd1YWdlKCdEb3VibGUgUHJvZHVjdCcpLCgxLDAsMCkpCiAgICAgICAgICAgIGJhLnBsYXlzb3VuZChiYS5nZXRzb3VuZCgnZXJyb3InKSkKCiAgICBkZWYgX19kZWxfXyhzZWxmKToKICAgICAgICBhcGdbJ0JlYXIgQ29pbiddID0gaW50KGFwZ1snQmVhciBDb2luJ10pCiAgICAgICAgYXBnLmFwcGx5X2FuZF9jb21taXQoKQoKY2xhc3MgUHJvbW9Db2RlOgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGNvZGU6IHN0ciA9ICcnKToKICAgICAgICBzZWxmLmNvZGUgPSBjb2RlCiAgICAgICAgc2VsZi5jb2Rlc19zdG9yZSA9IFNUT1JFWydQcm9tbyBDb2RlJ10KICAgICAgICBpZiBzZWxmLmNvZGUgaW4gc2VsZi5jb2Rlc19zdG9yZToKICAgICAgICAgICAgc2VsZi5jb2RlX3R5cGUgPSBTVE9SRVsnUHJvbW8gQ29kZSddW2NvZGVdCiAgICAgICAgICAgIHNlbGYucHJvbW9fY29kZV9leHBpcmUgPSBzZWxmLmNvZGVfdHlwZVswXQogICAgICAgICAgICBzZWxmLnByb21vX2NvZGVfYW1vdW50ID0gc2VsZi5jb2RlX3R5cGVbMV0KCiAgICBkZWYgX19kZWxfXyhzZWxmKToKICAgICAgICBhcGdbJ0JlYXIgQ29pbiddID0gaW50KGFwZ1snQmVhciBDb2luJ10pCiAgICAgICAgYXBnLmFwcGx5X2FuZF9jb21taXQoKQoKICAgIGRlZiBjb2RlX2NvbmZpcm1hdGlvbihzZWxmKToKICAgICAgICBpZiBzZWxmLmNvZGUgIT0gIiI6CiAgICAgICAgICAgIGJhLnNjcmVlbm1lc3NhZ2UoCiAgICAgICAgICAgICAgICBiYS5Mc3RyKHJlc291cmNlPSdzdWJtaXR0aW5nUHJvbW9Db2RlVGV4dCcpLCgwLDEsMCkpCiAgICAgICAgICAgIGJhLnRpbWVyKDIsYmEuQ2FsbChzZWxmLnZhbGlkYXRlX2NvZGUpKQoKICAgIGRlZiB2YWxpZGF0ZV9jb2RlKHNlbGYpOgogICAgICAgIGlmIHNlbGYuY29kZSBpbiBzZWxmLmNvZGVzX3N0b3JlOgogICAgICAgICAgICBpZiBzZWxmLnByb21vX2NvZGVfZXhwaXJlOgogICAgICAgICAgICAgICAgYmEudGltZXIoMS41LGJhLkNhbGwoc2VsZi5zdWNjZXNzZnVsX2NvZGUpKQogICAgICAgICAgICAgICAgYmEuc2NyZWVubWVzc2FnZShnZXRsYW5ndWFnZSgnVHJ1ZSBDb2RlJyksKDAsMSwwKSkKICAgICAgICAgICAgICAgIGJhLnBsYXlzb3VuZChiYS5nZXRzb3VuZCgnY2hlZXInKSkKICAgICAgICAgICAgICAgIHNlbGYuY29kZV90eXBlWzBdID0gRmFsc2UKICAgICAgICAgICAgZWxzZToKICAgICAgICAgICAgICAgIGJhLnNjcmVlbm1lc3NhZ2UoZ2V0bGFuZ3VhZ2UoJ0ZhbHNlIENvZGUnKSwoMSwwLDApKQogICAgICAgICAgICAgICAgYmEucGxheXNvdW5kKGJhLmdldHNvdW5kKCdlcnJvcicpKQogICAgICAgIGVsc2U6CiAgICAgICAgICAgIGJhLnNjcmVlbm1lc3NhZ2UoZ2V0bGFuZ3VhZ2UoJ0ludmFsaWQgQ29kZScpLCgxLDAsMCkpCiAgICAgICAgICAgIGJhLnBsYXlzb3VuZChiYS5nZXRzb3VuZCgnZXJyb3InKSkKCiAgICBkZWYgc3VjY2Vzc2Z1bF9jb2RlKHNlbGYpOgogICAgICAgIGFwZ1snQmVhciBDb2luJ10gKz0gc2VsZi5wcm9tb19jb2RlX2Ftb3VudAogICAgICAgIGJhLnNjcmVlbm1lc3NhZ2UoZ2V0bGFuZ3VhZ2UoJ1Jld2FyZCBDb2RlJywKICAgICAgICAgICAgc3Vicz1zZWxmLnByb21vX2NvZGVfYW1vdW50KSwoMCwxLDApKQogICAgICAgIGJhLnBsYXlzb3VuZChiYS5nZXRzb3VuZCgnY2FzaFJlZ2lzdGVyMicpKQoKTWFpbk1lbnVBY3Rpdml0eS5zdXBlcl90cmFuc2l0aW9uX2luID0gTWFpbk1lbnVBY3Rpdml0eS5vbl90cmFuc2l0aW9uX2luCmRlZiBuZXdfb25fdHJhbnNpdGlvbl9pbihzZWxmKToKICAgIHNlbGYuc3VwZXJfdHJhbnNpdGlvbl9pbigpCiAgICBsaW1pdCA9IDg0MDAKICAgIGJlYXJfY29pbiA9IGFwZ1snQmVhciBDb2luJ10KICAgIGNvaW5zX21lc3NhZ2UgPSBHTE9CQUxbJ0NvaW5zIE1lc3NhZ2UnXQogICAgdHJ5OgogICAgICAgIGlmIG5vdCAoU1RPUkVbJ0J1eSBGaXJlYm9tYnMnXSBhbmQKICAgICAgICAgICAgICAgIFNUT1JFWydCdXkgT3B0aW9uJ10gYW5kCiAgICAgICAgICAgICAgICBTVE9SRVsnQnV5IFBlcmNlbnRhZ2UnXSk6CiAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgIGlmIGNvaW5zX21lc3NhZ2UgIT0gW106CiAgICAgICAgICAgICAgICByZXN1bHQgPSAwCiAgICAgICAgICAgICAgICBmb3IgaSBpbiBjb2luc19tZXNzYWdlOgogICAgICAgICAgICAgICAgICAgIHJlc3VsdCArPSBpCgogICAgICAgICAgICAgICAgaWYgbm90IGJlYXJfY29pbiA+PSAobGltaXQtMSk6CiAgICAgICAgICAgICAgICAgICAgYmEuc2NyZWVubWVzc2FnZShnZXRsYW5ndWFnZSgnQ29pbnMgTWVzc2FnZScsc3Vicz1yZXN1bHQpLCgwLDEsMCkpCiAgICAgICAgICAgICAgICAgICAgYmEucGxheXNvdW5kKGJhLmdldHNvdW5kKCdjYXNoUmVnaXN0ZXInKSkKICAgICAgICAgICAgICAgIGVsc2U6CiAgICAgICAgICAgICAgICAgICAgYmEuc2NyZWVubWVzc2FnZShnZXRsYW5ndWFnZSgnQ29pbnMgTGltaXQgTWVzc2FnZScsCiAgICAgICAgICAgICAgICAgICAgICAgIGFsbWFjZW49W3Jlc3VsdCxsaW1pdF0pLCgxLDAsMCkpCiAgICAgICAgICAgICAgICAgICAgYmEucGxheXNvdW5kKGJhLmdldHNvdW5kKCdlcnJvcicpKQogICAgICAgICAgICAgICAgc2VsZi5iZWFyX2NvaW5fbWVzc2FnZSA9IFRydWUKICAgICAgICAgICAgICAgIEdMT0JBTFsnQ29pbnMgTWVzc2FnZSddID0gW10KICAgIGV4Y2VwdDogcGFzcwoKU3BhekJvdC5zdXBlcl9oYW5kbGVtZXNzYWdlID0gU3BhekJvdC5oYW5kbGVtZXNzYWdlCmRlZiBib3RfaGFuZGxlbWVzc2FnZShzZWxmLCBtc2c6IEFueSk6CiAgICBzZWxmLnN1cGVyX2hhbmRsZW1lc3NhZ2UobXNnKQogICAgaWYgaXNpbnN0YW5jZShtc2csIGJhLkRpZU1lc3NhZ2UpOgogICAgICAgIGlmIG5vdCBzZWxmLmRpZToKICAgICAgICAgICAgc2VsZi5kaWUgPSBUcnVlCiAgICAgICAgICAgIHNlbGYubGltaXQgPSA4NDAwCiAgICAgICAgICAgIHNlbGYuZnJlZV9jb2lucyA9IHJhbmRvbS5yYW5kaW50KDEsMjUpCiAgICAgICAgICAgIHNlbGYuYmVhcl9jb2lucyA9IGFwZ1snQmVhciBDb2luJ10KICAgICAgICAgICAgCiAgICAgICAgICAgIGlmIG5vdCBzZWxmLmJlYXJfY29pbnMgPj0gKHNlbGYubGltaXQpOgogICAgICAgICAgICAgICAgc2VsZi5iZWFyX2NvaW5zICs9IHNlbGYuZnJlZV9jb2lucwogICAgICAgICAgICAgICAgR0xPQkFMWydDb2lucyBNZXNzYWdlJ10uYXBwZW5kKHNlbGYuZnJlZV9jb2lucykKCiAgICAgICAgICAgICAgICBpZiBzZWxmLmJlYXJfY29pbnMgPj0gKHNlbGYubGltaXQpOgogICAgICAgICAgICAgICAgICAgIHNlbGYuYmVhcl9jb2lucyA9IHNlbGYubGltaXQKICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgIGFwZ1snQmVhciBDb2luJ10gPSBpbnQoc2VsZi5iZWFyX2NvaW5zKQogICAgICAgICAgICAgICAgYXBnLmFwcGx5X2FuZF9jb21taXQoKQogICAgICAgICAgICAgICAgCiAgICAgICAgICAgIGVsc2U6IEdMT0JBTFsnQ29pbnMgTWVzc2FnZSddLmFwcGVuZChzZWxmLmZyZWVfY29pbnMpCgpkZWYgY2xzX3Bvd19jb2xvcigpOgogICAgcmV0dXJuIFsoMSwwLjEsMC4xKSwoMC4xLDAuNSwwLjkpLCgwLjEsMC45LDAuOSksCiAgICAgICAgICAgICgwLjEsMC45LDAuMSksKDAuMSwxLDAuNSksKDEsMSwwLjIpLCgyLDAuNSwwLjUpLCgxLDAsNildCgpkZWYgcmFuZG9tX2NvbG9yKCk6CiAgICBhID0gcmFuZG9tLnJhbmRvbSgpKjMKICAgIGIgPSByYW5kb20ucmFuZG9tKCkqMwogICAgYyA9IHJhbmRvbS5yYW5kb20oKSozCiAgICByZXR1cm4gKGEsYixjKQoKZGVmIHBvd2VydXBfZGlzdCgpOgogICAgcmV0dXJuICgoJ3RyaXBsZV9ib21icycsIHBvd2VydXBzWydUcmlwbGUnXSksCiAgICAgICAgICAgICgnaWNlX2JvbWJzJywgcG93ZXJ1cHNbJ0ljZSBCb21icyddKSwKICAgICAgICAgICAgKCdwdW5jaCcsIHBvd2VydXBzWydQdW5jaCddKSwKICAgICAgICAgICAgKCdpbXBhY3RfYm9tYnMnLCBwb3dlcnVwc1snSW1wYWN0IEJvbWJzJ10pLAogICAgICAgICAgICAoJ2xhbmRfbWluZXMnLCBwb3dlcnVwc1snTWluZSBCb21icyddKSwKICAgICAgICAgICAgKCdzdGlja3lfYm9tYnMnLCBwb3dlcnVwc1snU3RpY2t5IEJvbWJzJ10pLAogICAgICAgICAgICAoJ3NoaWVsZCcsIHBvd2VydXBzWydTaGllbGQnXSksCiAgICAgICAgICAgICgnaGVhbHRoJywgcG93ZXJ1cHNbJ0hlYWx0aCddKSwKICAgICAgICAgICAgKCdjdXJzZScsIHBvd2VydXBzWydDdXJzZSddKSwKICAgICAgICAgICAgKCdzcGVlZCcscG93ZXJ1cHNbJ1NwZWVkJ10pLAogICAgICAgICAgICAoJ2hlYWx0aF9kYW1hZ2UnLCBwb3dlcnVwc1snSGVhbGluZyBEYW1hZ2UnXSksCiAgICAgICAgICAgICgnZ29vZGJ5ZScscG93ZXJ1cHNbJ0dvb2RieWUnXSksCiAgICAgICAgICAgICgnaWNlX21hbicscG93ZXJ1cHNbJ0ljZSBNYW4nXSksCiAgICAgICAgICAgICgndGFua19zaGllbGQnLHBvd2VydXBzWydUYW5rIFNoaWVsZCddKSwKICAgICAgICAgICAgKCdpbXBhaXJtZW50X2JvbWJzJyxwb3dlcnVwc1snSW1wYWlybWVudCBCb21icyddKSwKICAgICAgICAgICAgKCdmaXJlX2JvbWJzJyxwb3dlcnVwc1snRmlyZSBCb21icyddKSwKICAgICAgICAgICAgKCdmbHlfYm9tYnMnLHBvd2VydXBzWydGbHkgQm9tYnMnXSkpCgpkZWYgcGVyY2VudGFnZV90YW5rX3NoaWVsZCgpOgogICAgcGVyY2VudGFnZSA9IGNvbmZpZ1snVGFuayBTaGllbGQgUFRHJ10KICAgIHBlcmNlbnRhZ2VfdGV4dCA9ICgnMC4nKSArIHN0cihwZXJjZW50YWdlKQogICAgcmV0dXJuIGZsb2F0KHBlcmNlbnRhZ2VfdGV4dCkKICAgIApkZWYgcGVyY2VudGFnZV9oZWFsdGhfZGFtYWdlKCk6CiAgICBwZXJjZW50YWdlID0gY29uZmlnWydIZWFsaW5nIERhbWFnZSBQVEcnXQogICAgcGVyY2VudGFnZV90ZXh0ID0gKCcwLicpICsgc3RyKHBlcmNlbnRhZ2UpCiAgICByZXR1cm4gZmxvYXQocGVyY2VudGFnZV90ZXh0KQoKIyA9PT0gTW9kaWZ5IGNsYXNzID09PQoKY2xhc3MgTmV3UHJvZmlsZUJyb3dzZXJXaW5kb3coYnJvd3Nlci5Qcm9maWxlQnJvd3NlcldpbmRvdyk6CiAgICBkZWYgX19pbml0X18oc2VsZiwKICAgICAgICAgICAgICAgICB0cmFuc2l0aW9uOiBzdHIgPSAnaW5fcmlnaHQnLAogICAgICAgICAgICAgICAgIGluX21haW5fbWVudTogYm9vbCA9IFRydWUsCiAgICAgICAgICAgICAgICAgc2VsZWN0ZWRfcHJvZmlsZTogc3RyID0gTm9uZSwKICAgICAgICAgICAgICAgICBvcmlnaW5fd2lkZ2V0OiBiYS5XaWRnZXQgPSBOb25lKToKICAgICAgICBzdXBlcigpLl9faW5pdF9fKHRyYW5zaXRpb24saW5fbWFpbl9tZW51LHNlbGVjdGVkX3Byb2ZpbGUsb3JpZ2luX3dpZGdldCkKICAgICAgIAogICAgICAgIHNlbGYuc2Vzc2lvbiA9IF9iYS5nZXRfZm9yZWdyb3VuZF9ob3N0X3Nlc3Npb24oKQogICAgICAgIHVpc2NhbGUgPSBiYS5hcHAudWkudWlzY2FsZQogICAgICAgIHdpZHRoID0gKDEwMCBpZiB1aXNjYWxlIGlzCiAgICAgICAgICAgICAgICAgYmEuVUlTY2FsZS5TTUFMTCBlbHNlIC0xNCkKICAgICAgICBzaXplID0gNTAKICAgICAgICBwb3NpdGlvbiA9ICh3aWR0aCoxLjY1LDMwMCkKIAogICAgICAgIGlmIGlzaW5zdGFuY2Uoc2VsZi5zZXNzaW9uLE1haW5NZW51U2Vzc2lvbik6CiAgICAgICAgICAgIHNlbGYuYnV0dG9uID0gYmEuYnV0dG9ud2lkZ2V0KHBhcmVudD1zZWxmLl9yb290X3dpZGdldCwKICAgICAgICAgICAgICAgICAgICAgICAgICBhdXRvc2VsZWN0PVRydWUscG9zaXRpb249cG9zaXRpb24sCiAgICAgICAgICAgICAgICAgICAgICAgICAgc2l6ZT0oc2l6ZSxzaXplKSxidXR0b25fdHlwZT0nc3F1YXJlJywKICAgICAgICAgICAgICAgICAgICAgICAgICBsYWJlbD0nJyxvbl9hY3RpdmF0ZV9jYWxsPWJhLkNhbGwoc2VsZi5wb3dlcnVwbWFuYWdlcl93aW5kb3cpKQogICAgICAgICAgICAKICAgICAgICAgICAgc2l6ZSA9IHNpemUqMC42MAogICAgICAgICAgICBzZWxmLmltYWdlID0gYmEuaW1hZ2V3aWRnZXQocGFyZW50PXNlbGYuX3Jvb3Rfd2lkZ2V0LAogICAgICAgICAgICAgICAgICAgICAgICAgIHNpemU9KHNpemUsc2l6ZSksZHJhd19jb250cm9sbGVyPXNlbGYuYnV0dG9uLAogICAgICAgICAgICAgICAgICAgICAgICAgIHBvc2l0aW9uPShwb3NpdGlvblswXSsxMC41LHBvc2l0aW9uWzFdKzE3KSwKICAgICAgICAgICAgICAgICAgICAgICAgICB0ZXh0dXJlPWJhLmdldHRleHR1cmUoJ3Bvd2VydXBTcGVlZCcpKQogICAgCiAgICAgICAgICAgIHNlbGYudGV4dCA9IGJhLnRleHR3aWRnZXQocGFyZW50PXNlbGYuX3Jvb3Rfd2lkZ2V0LAogICAgICAgICAgICAgICAgICAgICAgICAgIHBvc2l0aW9uPShwb3NpdGlvblswXSsyNSxwb3NpdGlvblsxXSsxMCksCiAgICAgICAgICAgICAgICAgICAgICAgICAgc2l6ZT0oMCwgMCksc2NhbGU9MC40NSxjb2xvcj0oMC43LDAuOSwwLjcsMS4wKSwKICAgICAgICAgICAgICAgICAgICAgICAgICBkcmF3X2NvbnRyb2xsZXI9c2VsZi5idXR0b24sbWF4d2lkdGg9NjAsCiAgICAgICAgICAgICAgICAgICAgICAgICAgdGV4dD0oZiJVbHRpbWF0ZSBQb3dlcnVwIHtfc3BffU1hbmFnZXIiKSxoX2FsaWduPSdjZW50ZXInLHZfYWxpZ249J2NlbnRlcicpCgogICAgZGVmIHBvd2VydXBtYW5hZ2VyX3dpbmRvdyhzZWxmKToKICAgICAgICBiYS5jb250YWluZXJ3aWRnZXQoZWRpdD1zZWxmLl9yb290X3dpZGdldCx0cmFuc2l0aW9uPSdvdXRfbGVmdCcpCiAgICAgICAgUG93ZXJ1cE1hbmFnZXJXaW5kb3coKQoKY2xhc3MgTmV3UG93ZXJ1cEJveEZhY3RvcnkocHVwYm94LlBvd2VydXBCb3hGYWN0b3J5KToKICAgIGRlZiBfX2luaXRfXyhzZWxmKSAtPiBOb25lOgogICAgICAgIHN1cGVyKCkuX19pbml0X18oKQogICAgICAgIHNlbGYudGV4X3NwZWVkID0gYmEuZ2V0dGV4dHVyZSgncG93ZXJ1cFNwZWVkJykKICAgICAgICBzZWxmLnRleF9oZWFsdGhfZGFtYWdlID0gYmEuZ2V0dGV4dHVyZSgnaGVhcnQnKQogICAgICAgIHNlbGYudGV4X2dvb2RieWUgPSBiYS5nZXR0ZXh0dXJlKCdhY2hpZXZlbWVudE9uc2xhdWdodCcpCiAgICAgICAgc2VsZi50ZXhfaWNlX21hbiA9IGJhLmdldHRleHR1cmUoJ291eWFVQnV0dG9uJykKICAgICAgICBzZWxmLnRleF90YW5rX3NoaWVsZCA9IGJhLmdldHRleHR1cmUoJ2FjaGlldmVtZW50U3VwZXJQdW5jaCcpCiAgICAgICAgc2VsZi50ZXhfaW1wYWlybWVudF9ib21icyA9IGJhLmdldHRleHR1cmUoJ2xldmVsSWNvbicpCiAgICAgICAgc2VsZi50ZXhfZmlyZV9ib21icyA9IGJhLmdldHRleHR1cmUoJ291eWFPQnV0dG9uJykKICAgICAgICBzZWxmLnRleF9mbHlfYm9tYnMgPSBiYS5nZXR0ZXh0dXJlKCdzdGFyJykKICAgICAgICAKICAgICAgICBzZWxmLl9wb3dlcnVwZGlzdCA9IFtdCiAgICAgICAgZm9yIHBvd2VydXAsIGZyZXEgaW4gcG93ZXJ1cF9kaXN0KCk6CiAgICAgICAgICAgIGZvciBfaSBpbiByYW5nZShpbnQoZnJlcSkpOgogICAgICAgICAgICAgICAgc2VsZi5fcG93ZXJ1cGRpc3QuYXBwZW5kKHBvd2VydXApCgogICAgZGVmIGdldF9yYW5kb21fcG93ZXJ1cF90eXBlKHNlbGYsZm9yY2V0eXBlID0gTm9uZSwgZXhjbHVkZXR5cGVzID0gTm9uZSk6CiAgICAgICAgCiAgICAgICAgdHJ5OiBzZWxmLm1hcGEgPSBiYS5nZXRhY3Rpdml0eSgpLl9tYXAuZ2V0bmFtZSgpCiAgICAgICAgZXhjZXB0OiBzZWxmLm1hcGEgPSBOb25lCiAgICAgIAogICAgICAgIHNwZWVkX2Jhbm5lZF9tYXBzID0gWydIb2NrZXkgU3RhZGl1bScsJ0xha2UgRnJpZ2lkJywnSGFwcHkgVGhvdWdodHMnXQogICAgICAKICAgICAgICBpZiBzZWxmLm1hcGEgaW4gc3BlZWRfYmFubmVkX21hcHM6CiAgICAgICAgICAgIHBvd2VydXBfZGlzYWJsZSA9IFsnc3BlZWQnXQogICAgICAgIGVsc2U6IHBvd2VydXBfZGlzYWJsZSA9IFtdCiAgICAgIAogICAgICAgIGlmIGV4Y2x1ZGV0eXBlcyBpcyBOb25lOgogICAgICAgICAgICBleGNsdWRldHlwZXMgPSBbXQogICAgICAgIGlmIGZvcmNldHlwZToKICAgICAgICAgICAgcHR5cGUgPSBmb3JjZXR5cGUKICAgICAgICBlbHNlOgogICAgICAgICAgICBpZiBzZWxmLl9sYXN0cG93ZXJ1cHR5cGUgPT0gJ2N1cnNlJzoKICAgICAgICAgICAgICAgIHB0eXBlID0gJ2hlYWx0aCcKICAgICAgICAgICAgZWxzZToKICAgICAgICAgICAgICAgIHdoaWxlIFRydWU6CiAgICAgICAgICAgICAgICAgICAgcHR5cGUgPSBzZWxmLl9wb3dlcnVwZGlzdFtyYW5kb20ucmFuZGludCgKICAgICAgICAgICAgICAgICAgICAgICAgMCwKICAgICAgICAgICAgICAgICAgICAgICAgbGVuKHNlbGYuX3Bvd2VydXBkaXN0KSAtIDEpXQogICAgICAgICAgICAgICAgICAgIGlmIHB0eXBlIG5vdCBpbiBleGNsdWRldHlwZXMgYW5kIHB0eXBlIG5vdCBpbiBwb3dlcnVwX2Rpc2FibGU6IGJyZWFrCiAgICAgICAgc2VsZi5fbGFzdHBvd2VydXB0eXBlID0gcHR5cGUKICAgICAgICByZXR1cm4gcHR5cGUKCmRlZiBmaXJlX2VmZmVjdChzZWxmKToKICAgIGlmIHNlbGYubm9kZS5leGlzdHMoKToKICAgICAgICBiYS5lbWl0ZngocG9zaXRpb249c2VsZi5ub2RlLnBvc2l0aW9uLAogICAgICAgIHNjYWxlPTMsY291bnQ9NTAqMixzcHJlYWQ9MC4zLAogICAgICAgIGNodW5rX3R5cGU9J3N3ZWF0JykKICAgIGVsc2U6IHNlbGYuZmlyZV9lZmZlY3RfdGltZSA9IE5vbmUKCiMjIyMjIyMjIyMjQk9NQlMKQm9tYi5fcG1fb2xkX2JvbWIgPSBCb21iLl9faW5pdF9fCmRlZiBfYm9tYl9pbml0KHNlbGYsCiAgICAgICAgICAgICAgIHBvc2l0aW9uOiBTZXF1ZW5jZVtmbG9hdF0gPSAoMC4wLCAxLjAsIDAuMCksCiAgICAgICAgICAgICAgIHZlbG9jaXR5OiBTZXF1ZW5jZVtmbG9hdF0gPSAoMC4wLCAwLjAsIDAuMCksCiAgICAgICAgICAgICAgIGJvbWJfdHlwZTogc3RyID0gJ25vcm1hbCcsCiAgICAgICAgICAgICAgIGJsYXN0X3JhZGl1czogZmxvYXQgPSAyLjAsCiAgICAgICAgICAgICAgIGJvbWJfc2NhbGU6IGZsb2F0ID0gMS4wLAogICAgICAgICAgICAgICBzb3VyY2VfcGxheWVyOiBiYS5QbGF5ZXIgPSBOb25lLAogICAgICAgICAgICAgICBvd25lcjogYmEuTm9kZSA9IE5vbmUpOgoKICAgIHNlbGYuYm1fdHlwZSA9IGJvbWJfdHlwZQogICAgbmV3X2JvbWJfdHlwZSA9IGJvbWJfdHlwZQogICAgYm9tYnMgPSBbJ2ljZV9idWJibGUnLCdpbXBhaXJtZW50JywnZmlyZScsJ2ZseSddCiAgICAKICAgIGlmIGJvbWJfdHlwZSBpbiBib21iczoKICAgICAgICBuZXdfYm9tYl90eXBlID0gJ2ljZScKICAgICAgICAgICAgICAgICAgIAogICAgc2VsZi5fcG1fb2xkX2JvbWIocG9zaXRpb24sdmVsb2NpdHksbmV3X2JvbWJfdHlwZSxibGFzdF9yYWRpdXMsCiAgICAgICAgICAgICAgICAgICAgICBib21iX3NjYWxlLHNvdXJjZV9wbGF5ZXIsb3duZXIpCiAgICAKICAgIHRleCA9IHNlbGYubm9kZS5jb2xvcl90ZXh0dXJlCiAgICAKICAgIGlmIHNlbGYuYm1fdHlwZSA9PSAnaWNlX2J1YmJsZSc6CiAgICAgICAgc2VsZi5ib21iX3R5cGUgPSBzZWxmLmJtX3R5cGUKICAgICAgICBzZWxmLm5vZGUubW9kZWwgPSBOb25lCiAgICAgICAgc2VsZi5zaGllbGRfaWNlID0gYmEubmV3bm9kZSgnc2hpZWxkJyxvd25lcj1zZWxmLm5vZGUsCiAgICAgICAgICAgIGF0dHJzPXsnY29sb3InOiAoMC41LCAxLjAsIDcuMCksJ3JhZGl1cyc6IDAuNn0pCiAgICAgICAgc2VsZi5ub2RlLmNvbm5lY3RhdHRyKCdwb3NpdGlvbicsIHNlbGYuc2hpZWxkX2ljZSwgJ3Bvc2l0aW9uJykKICAgIGVsaWYgc2VsZi5ibV90eXBlID09ICdmaXJlJzoKICAgICAgICBzZWxmLmJvbWJfdHlwZSA9IHNlbGYuYm1fdHlwZQogICAgICAgIHNlbGYubm9kZS5tb2RlbCA9IE5vbmUKICAgICAgICBzZWxmLnNoaWVsZF9maXJlID0gYmEubmV3bm9kZSgnc2hpZWxkJyxvd25lcj1zZWxmLm5vZGUsCiAgICAgICAgICAgIGF0dHJzPXsnY29sb3InOiAoNi41LCA2LjUsIDIuMCksJ3JhZGl1cyc6IDAuNn0pCiAgICAgICAgc2VsZi5ub2RlLmNvbm5lY3RhdHRyKCdwb3NpdGlvbicsIHNlbGYuc2hpZWxkX2ZpcmUsICdwb3NpdGlvbicpCiAgICAgICAgc2VsZi5maXJlX2VmZmVjdF90aW1lID0gYmEuVGltZXIoMC4xLGJhLkNhbGwoZmlyZV9lZmZlY3Qsc2VsZikscmVwZWF0PVRydWUpCiAgICBlbGlmIHNlbGYuYm1fdHlwZSA9PSAnaW1wYWlybWVudCc6CiAgICAgICAgc2VsZi5ib21iX3R5cGUgPSBzZWxmLmJtX3R5cGUKICAgICAgICB0ZXggPSBiYS5nZXR0ZXh0dXJlKCdlZ2dUZXgzJykKICAgIGVsaWYgc2VsZi5ibV90eXBlID09ICdmbHknOgogICAgICAgIHNlbGYuYm9tYl90eXBlID0gc2VsZi5ibV90eXBlCiAgICAgICAgdGV4ID0gYmEuZ2V0dGV4dHVyZSgnZWdnVGV4MScpCgogICAgc2VsZi5ub2RlLmNvbG9yX3RleHR1cmUgPSB0ZXgKICAgIHNlbGYuaGl0X3N1YnR5cGUgPSBzZWxmLmJvbWJfdHlwZQoKICAgIGlmIHNlbGYuYm9tYl90eXBlID09ICdpY2VfYnViYmxlJzoKICAgICAgICBzZWxmLmJsYXN0X3JhZGl1cyAqPSAxLjIKICAgIGVsaWYgc2VsZi5ib21iX3R5cGUgPT0gJ2ZseSc6CiAgICAgICAgc2VsZi5ibGFzdF9yYWRpdXMgKj0gMi4yCgpkZWYgYm9tYl9oYW5kbGVtZXNzYWdlKHNlbGYsIG1zZzogQW55KSAtPiBBbnk6CiAgICBhc3NlcnQgbm90IHNlbGYuZXhwaXJlZAoKICAgIGlmIGlzaW5zdGFuY2UobXNnLCBiYS5EaWVNZXNzYWdlKToKICAgICAgICBpZiBzZWxmLm5vZGU6CiAgICAgICAgICAgIHNlbGYubm9kZS5kZWxldGUoKQoKICAgIGVsaWYgaXNpbnN0YW5jZShtc2csIGJvbWIuRXhwbG9kZUhpdE1lc3NhZ2UpOgogICAgICAgIG5vZGUgPSBiYS5nZXRjb2xsaXNpb24oKS5vcHBvc2luZ25vZGUKICAgICAgICBhc3NlcnQgc2VsZi5ub2RlCiAgICAgICAgbm9kZXBvcyA9IHNlbGYubm9kZS5wb3NpdGlvbgogICAgICAgIG1hZyA9IDIwMDAuMAogICAgICAgIGlmIHNlbGYuYmxhc3RfdHlwZSBpbiAoJ2ljZScsJ2ljZV9idWJibGUnKToKICAgICAgICAgICAgbWFnICo9IDAuNQogICAgICAgIGVsaWYgc2VsZi5ibGFzdF90eXBlID09ICdsYW5kX21pbmUnOgogICAgICAgICAgICBtYWcgKj0gMi41CiAgICAgICAgZWxpZiBzZWxmLmJsYXN0X3R5cGUgPT0gJ3RudCc6CiAgICAgICAgICAgIG1hZyAqPSAyLjAKICAgICAgICBlbGlmIHNlbGYuYmxhc3RfdHlwZSA9PSAnZmlyZSc6CiAgICAgICAgICAgIG1hZyAqPSAwLjYKICAgICAgICBlbGlmIHNlbGYuYmxhc3RfdHlwZSA9PSAnZmx5JzoKICAgICAgICAgICAgbWFnICo9IDUuNQoKICAgICAgICBub2RlLmhhbmRsZW1lc3NhZ2UoCiAgICAgICAgICAgIGJhLkhpdE1lc3NhZ2UocG9zPW5vZGVwb3MsCiAgICAgICAgICAgICAgICAgICAgICAgICAgdmVsb2NpdHk9KDAsIDAsIDApLAogICAgICAgICAgICAgICAgICAgICAgICAgIG1hZ25pdHVkZT1tYWcsCiAgICAgICAgICAgICAgICAgICAgICAgICAgaGl0X3R5cGU9c2VsZi5oaXRfdHlwZSwKICAgICAgICAgICAgICAgICAgICAgICAgICBoaXRfc3VidHlwZT1zZWxmLmhpdF9zdWJ0eXBlLAogICAgICAgICAgICAgICAgICAgICAgICAgIHJhZGl1cz1zZWxmLnJhZGl1cywKICAgICAgICAgICAgICAgICAgICAgICAgICBzb3VyY2VfcGxheWVyPWJhLmV4aXN0aW5nKHNlbGYuX3NvdXJjZV9wbGF5ZXIpKSkKICAgICAgICBpZiBzZWxmLmJsYXN0X3R5cGUgaW4gKCdpY2UnLCdpY2VfYnViYmxlJyk6CiAgICAgICAgICAgIGJhLnBsYXlzb3VuZChib21iLkJvbWJGYWN0b3J5LmdldCgpLmZyZWV6ZV9zb3VuZCwKICAgICAgICAgICAgICAgICAgICAgICAgIDEwLAogICAgICAgICAgICAgICAgICAgICAgICAgcG9zaXRpb249bm9kZXBvcykKICAgICAgICAgICAgbm9kZS5oYW5kbGVtZXNzYWdlKGJhLkZyZWV6ZU1lc3NhZ2UoKSkKCiAgICByZXR1cm4gTm9uZQoKZGVmIHBvd2VydXBfdHJhbnNsYXRlZChzZWxmLCB0eXBlOiBzdHIpOgogICAgcG93ZXJ1cHNfbmFtZXMgPSB7J3RyaXBsZV9ib21icyc6IGJhLkxzdHIocmVzb3VyY2U9J2hlbHBXaW5kb3cuJysncG93ZXJ1cEJvbWJOYW1lVGV4dCcpLAogICAgICAgICAgICAgICAgJ2ljZV9ib21icyc6IGJhLkxzdHIocmVzb3VyY2U9J2hlbHBXaW5kb3cuJysncG93ZXJ1cEljZUJvbWJzTmFtZVRleHQnKSwKICAgICAgICAgICAgICAgICdwdW5jaCc6IGJhLkxzdHIocmVzb3VyY2U9J2hlbHBXaW5kb3cuJysncG93ZXJ1cFB1bmNoTmFtZVRleHQnKSwKICAgICAgICAgICAgICAgICdpbXBhY3RfYm9tYnMnOiBiYS5Mc3RyKHJlc291cmNlPSdoZWxwV2luZG93LicrJ3Bvd2VydXBJbXBhY3RCb21ic05hbWVUZXh0JyksCiAgICAgICAgICAgICAgICAnbGFuZF9taW5lcyc6IGJhLkxzdHIocmVzb3VyY2U9J2hlbHBXaW5kb3cuJysncG93ZXJ1cExhbmRNaW5lc05hbWVUZXh0JyksCiAgICAgICAgICAgICAgICAnc3RpY2t5X2JvbWJzJzogYmEuTHN0cihyZXNvdXJjZT0naGVscFdpbmRvdy4nKydwb3dlcnVwU3RpY2t5Qm9tYnNOYW1lVGV4dCcpLAogICAgICAgICAgICAgICAgJ3NoaWVsZCc6IGJhLkxzdHIocmVzb3VyY2U9J2hlbHBXaW5kb3cuJysncG93ZXJ1cFNoaWVsZE5hbWVUZXh0JyksCiAgICAgICAgICAgICAgICAnaGVhbHRoJzogYmEuTHN0cihyZXNvdXJjZT0naGVscFdpbmRvdy4nKydwb3dlcnVwSGVhbHRoTmFtZVRleHQnKSwKICAgICAgICAgICAgICAgICdjdXJzZSc6IGJhLkxzdHIocmVzb3VyY2U9J2hlbHBXaW5kb3cuJysncG93ZXJ1cEN1cnNlTmFtZVRleHQnKSwKICAgICAgICAgICAgICAgICdzcGVlZCc6IGdldGxhbmd1YWdlKCdTcGVlZCcpLAogICAgICAgICAgICAgICAgJ2hlYWx0aF9kYW1hZ2UnOiBnZXRsYW5ndWFnZSgnSGVhbGluZyBEYW1hZ2UnKSwKICAgICAgICAgICAgICAgICdnb29kYnllJzogZ2V0bGFuZ3VhZ2UoJ0dvb2RieWUnKSwKICAgICAgICAgICAgICAgICdpY2VfbWFuJzogZ2V0bGFuZ3VhZ2UoJ0ljZSBNYW4nKSwKICAgICAgICAgICAgICAgICd0YW5rX3NoaWVsZCc6IGdldGxhbmd1YWdlKCdUYW5rIFNoaWVsZCcpLAogICAgICAgICAgICAgICAgJ2ltcGFpcm1lbnRfYm9tYnMnOiBnZXRsYW5ndWFnZSgnSW1wYWlybWVudCBCb21icycpLAogICAgICAgICAgICAgICAgJ2ZpcmVfYm9tYnMnOiBnZXRsYW5ndWFnZSgnRmlyZSBCb21icycpLAogICAgICAgICAgICAgICAgJ2ZseV9ib21icyc6IGdldGxhbmd1YWdlKCdGbHkgQm9tYnMnKX0KICAgIHNlbGYudGV4dHNbJ05hbWUnXS50ZXh0ID0gcG93ZXJ1cHNfbmFtZXNbdHlwZV0KICAgICAgICAgICAgICAgIAojIyMjIyMjIyMjI1BPV0VSVVAKcHVwYm94LlBvd2VydXBCb3guX29sZF9wYnhfID0gcHVwYm94LlBvd2VydXBCb3guX19pbml0X18KZGVmIF9wYnhfKHNlbGYsIHBvc2l0aW9uOiBTZXF1ZW5jZVtmbG9hdF0gPSAoMC4wLCAxLjAsIDAuMCksCiAgICAgICAgICBwb3dlcnVwdHlwZTogc3RyID0gJ3RyaXBsZV9ib21icycsCiAgICAgICAgICBleHBpcmU6IGJvb2wgPSBUcnVlKToKICAgIAogICAgc2VsZi5uZXdzOiBsaXN0ID0gW10KICAgIGZvciB4LGkgaW4gcG93ZXJ1cF9kaXN0KCk6IHNlbGYubmV3cy5hcHBlbmQoeCkKICAgIAogICAgc2VsZi5ib3g6IGxpc3QgPSBbXQogICAgc2VsZi50ZXh0cyA9IHt9CiAgICBzZWxmLm5ld3MgPSBzZWxmLm5ld3NbOTpdCiAgICBzZWxmLmJveC5hcHBlbmQocG93ZXJ1cHR5cGUpCiAgICBzZWxmLm5wb3dlcnVwID0gc2VsZi5ib3hbMF0KICAgIGZhY3RvcnkgPSBOZXdQb3dlcnVwQm94RmFjdG9yeS5nZXQoKQogICAgCiAgICBpZiBzZWxmLm5wb3dlcnVwIGluIHNlbGYubmV3czogbmV3X3Bvd2VydXB0eXBlID0gJ3NoaWVsZCcKICAgIGVsc2U6IG5ld19wb3dlcnVwdHlwZSA9IHBvd2VydXB0eXBlCiAgICBzZWxmLl9vbGRfcGJ4Xyhwb3NpdGlvbixuZXdfcG93ZXJ1cHR5cGUsZXhwaXJlKQogICAgCiAgICB0eXBlID0gbmV3X3Bvd2VydXB0eXBlCiAgICB0ZXggPSBzZWxmLm5vZGUuY29sb3JfdGV4dHVyZQogICAgbW9kZWwgPSBzZWxmLm5vZGUubW9kZWwKICAgIAogICAgaWYgc2VsZi5ucG93ZXJ1cCA9PSAnc3BlZWQnOgogICAgICAgIHR5cGUgPSBzZWxmLm5wb3dlcnVwCiAgICAgICAgdGV4ID0gZmFjdG9yeS50ZXhfc3BlZWQKICAgIGVsaWYgc2VsZi5ucG93ZXJ1cCA9PSAnaGVhbHRoX2RhbWFnZSc6CiAgICAgICAgdHlwZSA9IHNlbGYubnBvd2VydXAKICAgICAgICB0ZXggPSBmYWN0b3J5LnRleF9oZWFsdGhfZGFtYWdlCiAgICBlbGlmIHNlbGYubnBvd2VydXAgPT0gJ2dvb2RieWUnOgogICAgICAgIHR5cGUgPSBzZWxmLm5wb3dlcnVwCiAgICAgICAgdGV4ID0gZmFjdG9yeS50ZXhfZ29vZGJ5ZQogICAgZWxpZiBzZWxmLm5wb3dlcnVwID09ICdpY2VfbWFuJzoKICAgICAgICB0eXBlID0gc2VsZi5ucG93ZXJ1cAogICAgICAgIHRleCA9IGZhY3RvcnkudGV4X2ljZV9tYW4KICAgIGVsaWYgc2VsZi5ucG93ZXJ1cCA9PSAndGFua19zaGllbGQnOgogICAgICAgIHR5cGUgPSBzZWxmLm5wb3dlcnVwCiAgICAgICAgdGV4ID0gZmFjdG9yeS50ZXhfdGFua19zaGllbGQKICAgIGVsaWYgc2VsZi5ucG93ZXJ1cCA9PSAnaW1wYWlybWVudF9ib21icyc6CiAgICAgICAgdHlwZSA9IHNlbGYubnBvd2VydXAKICAgICAgICB0ZXggPSBmYWN0b3J5LnRleF9pbXBhaXJtZW50X2JvbWJzCiAgICBlbGlmIHNlbGYubnBvd2VydXAgPT0gJ2ZpcmVfYm9tYnMnOgogICAgICAgIHR5cGUgPSBzZWxmLm5wb3dlcnVwCiAgICAgICAgdGV4ID0gZmFjdG9yeS50ZXhfZmlyZV9ib21icwogICAgZWxpZiBzZWxmLm5wb3dlcnVwID09ICdmbHlfYm9tYnMnOgogICAgICAgIHR5cGUgPSBzZWxmLm5wb3dlcnVwCiAgICAgICAgdGV4ID0gZmFjdG9yeS50ZXhfZmx5X2JvbWJzCgogICAgc2VsZi5wb3dlcnVwdHlwZSA9IHR5cGUKICAgIHNlbGYubm9kZS5tb2RlbCA9IG1vZGVsCiAgICBzZWxmLm5vZGUuY29sb3JfdGV4dHVyZSA9IHRleAogICAgbl9zY2FsZSA9IGNvbmZpZ1snUG93ZXJ1cCBTY2FsZSddCiAgICBzdHlsZSA9IGNvbmZpZ1snUG93ZXJ1cCBTdHlsZSddCgogICAgY3VydmUgPSBiYS5hbmltYXRlKHNlbGYubm9kZSwgJ21vZGVsX3NjYWxlJywgezA6IDAsIDAuMTQ6IDEuNiwgMC4yOiBuX3NjYWxlfSkKICAgIGJhLnRpbWVyKDAuMiwgY3VydmUuZGVsZXRlKQogICAgICAgIAogICAgZGVmIHV0aWxfdGV4dCh0eXBlOiBzdHIsIHRleHQ6IHN0ciwgc2NhbGU6IGZsb2F0ID0gMSwgY29sb3I6IGxpc3QgPSBbMSwxLDFdLAogICAgICAgICAgICAgICAgICBwb3NpdGlvbjogbGlzdCA9IFswLCAwLjcsIDBdLCBjb2xvcnNfbmFtZTogYm9vbCA9IEZhbHNlKToKICAgICAgICBtID0gYmEubmV3bm9kZSgnbWF0aCcsb3duZXI9c2VsZi5ub2RlLGF0dHJzPXsnaW5wdXQxJzoKICAgICAgICAgICAgKHBvc2l0aW9uWzBdLCBwb3NpdGlvblsxXSwgcG9zaXRpb25bMl0pLCdvcGVyYXRpb24nOiAnYWRkJ30pCiAgICAgICAgc2VsZi5ub2RlLmNvbm5lY3RhdHRyKCdwb3NpdGlvbicsIG0sICdpbnB1dDInKQogICAgICAgIHNlbGYudGV4dHNbdHlwZV0gPSBiYS5uZXdub2RlKCd0ZXh0Jyxvd25lcj1zZWxmLm5vZGUsCiAgICAgICAgICAgICAgICBhdHRycz17J3RleHQnOiBzdHIodGV4dCksCiAgICAgICAgICAgICAgICAgICAgICAnaW5fd29ybGQnOiBUcnVlLAogICAgICAgICAgICAgICAgICAgICAgJ3NjYWxlJzogMC4wMiwKICAgICAgICAgICAgICAgICAgICAgICdzaGFkb3cnOiAwLjUsCiAgICAgICAgICAgICAgICAgICAgICAnZmxhdG5lc3MnOiAxLjAsCiAgICAgICAgICAgICAgICAgICAgICAnY29sb3InOiAoY29sb3JbMF0sY29sb3JbMV0sY29sb3JbMl0pLAogICAgICAgICAgICAgICAgICAgICAgJ2hfYWxpZ24nOiAnY2VudGVyJ30pIAogICAgICAgIG0uY29ubmVjdGF0dHIoJ291dHB1dCcsIHNlbGYudGV4dHNbdHlwZV0sICdwb3NpdGlvbicpCiAgICAgICAgYmEuYW5pbWF0ZShzZWxmLnRleHRzW3R5cGVdLCAnc2NhbGUnLCB7MDogMC4wMTcsMC40OiAwLjAxNywgMC41OiAwLjAxKnNjYWxlfSkKICAgIAogICAgICAgIGlmIGNvbG9yc19uYW1lOgogICAgICAgICAgICBiYS5hbmltYXRlX2FycmF5KHNlbGYudGV4dHNbdHlwZV0sICdjb2xvcicsIDMsCiAgICAgICAgICAgICAgICB7MDooMSwwLDApLAogICAgICAgICAgICAgICAgIDAuMjooMSwwLjUsMCksCiAgICAgICAgICAgICAgICAgMC40OigxLDEsMCksCiAgICAgICAgICAgICAgICAgMC42OigwLDEsMCksCiAgICAgICAgICAgICAgICAgMC44OigwLDEsMSksCiAgICAgICAgICAgICAgICAgMS4wOigxLDAsMSksCiAgICAgICAgICAgICAgICAgMS4yOigxLDAsMCl9LFRydWUpCiAgICAKICAgIGRlZiB1cGRhdGVfdGltZSh0aW1lKToKICAgICAgICBpZiBzZWxmLnRleHRzWydUaW1lJ10uZXhpc3RzKCk6CiAgICAgICAgICAgIHNlbGYudGV4dHNbJ1RpbWUnXS50ZXh0ID0gc3RyKHRpbWUpCiAgICAgICAgCiAgICBpZiBjb25maWdbJ1Bvd2VydXAgVGltZSddOgogICAgICAgIGludGVydmFsID0gaW50KHB1cGJveC5ERUZBVUxUX1BPV0VSVVBfSU5URVJWQUwpCiAgICAgICAgdGltZTIgPSAoaW50ZXJ2YWwtMSkKICAgICAgICB0aW1lID0gMQogICAgICAgIAogICAgICAgIHV0aWxfdGV4dCgnVGltZScsIHRpbWUyLCBzY2FsZT0xLjUsY29sb3I9KDIsMiwyKSwKICAgICAgICAgICAgICAgICAgcG9zaXRpb249WzAsMC45LDBdLCBjb2xvcnNfbmFtZT1GYWxzZSkKICAgICAgICAKICAgICAgICB3aGlsZShpbnRlcnZhbCszKToKICAgICAgICAgICAgYmEudGltZXIodGltZS0xLGJhLkNhbGwodXBkYXRlX3RpbWUsZid7dGltZTJ9cycpKQogICAgCiAgICAgICAgICAgIGlmIHRpbWUyID09IDA6CiAgICAgICAgICAgICAgICBicmVhawogICAgCiAgICAgICAgICAgIHRpbWUgKz0gMQogICAgICAgICAgICB0aW1lMiAtPSAxCiAgICAKICAgIGlmIGNvbmZpZ1snUG93ZXJ1cCBXaXRoIFNoaWVsZCddOgogICAgICAgIHNjYWxlID0gY29uZmlnWydQb3dlcnVwIFNjYWxlJ10KICAgICAgICBzZWxmLnNoaWVsZCA9IGJhLm5ld25vZGUoJ3NoaWVsZCcsb3duZXI9c2VsZi5ub2RlLGF0dHJzPXsnY29sb3InOiAoMSwxLDApLCdyYWRpdXMnOiAxLjMqc2NhbGV9KQogICAgICAgIHNlbGYubm9kZS5jb25uZWN0YXR0cigncG9zaXRpb24nLCBzZWxmLnNoaWVsZCwgJ3Bvc2l0aW9uJykKICAgICAgICBiYS5hbmltYXRlX2FycmF5KHNlbGYuc2hpZWxkLCdjb2xvcicsMyx7MDogKDIsMCwwKSwgMC41OiAoMCwyLDApLCAxOiAoMCwxLDYpLCAxLjU6ICgyLDAsMCl9LFRydWUpCiAgICAKICAgIGlmIGNvbmZpZ1snUG93ZXJ1cCBOYW1lJ106ICAgIAogICAgICAgIHV0aWxfdGV4dCgnTmFtZScsc2VsZi5wb3dlcnVwdHlwZSxzY2FsZT0xLjIsCiAgICAgICAgICAgICAgICAgIHBvc2l0aW9uPVswLDAuNCwwXSxjb2xvcnNfbmFtZT1UcnVlKQogICAgICAgIHBvd2VydXBfdHJhbnNsYXRlZChzZWxmLHNlbGYucG93ZXJ1cHR5cGUpCiAgICAgICAgCiAgICBpZiBzdHlsZSA9PSAnU1k6IEJBTEwnOgogICAgICAgIHNlbGYubm9kZS5tb2RlbCA9IGJhLmdldG1vZGVsKCdmcm9zdHlQZWx2aXMnKQogICAgZWxpZiBzdHlsZSA9PSAnU1k6IEltcGFjdCc6CiAgICAgICAgc2VsZi5ub2RlLm1vZGVsID0gYmEuZ2V0bW9kZWwoJ2ltcGFjdEJvbWInKQogICAgZWxpZiBzdHlsZSA9PSAnU1k6IEVnZyc6CiAgICAgICAgc2VsZi5ub2RlLm1vZGVsID0gYmEuZ2V0bW9kZWwoJ2VnZycpCiAgICAgICAgCiMjIyMjIyMjIyMjU1BBWgpkZWYgX3NwZWVkX29mZl9mbGFzaChzZWxmKToKICAgIGlmIHNlbGYubm9kZToKICAgICAgICBmYWN0b3J5ID0gTmV3UG93ZXJ1cEJveEZhY3RvcnkuZ2V0KCkKICAgICAgICBzZWxmLm5vZGUuYmlsbGJvYXJkX3RleHR1cmUgPSBmYWN0b3J5LnRleF9zcGVlZAogICAgICAgIHNlbGYubm9kZS5iaWxsYm9hcmRfb3BhY2l0eSA9IDEuMAogICAgICAgIHNlbGYubm9kZS5iaWxsYm9hcmRfY3Jvc3Nfb3V0ID0gVHJ1ZQogICAgICAgIApkZWYgX3NwZWVkX3dlYXJfb2ZmKHNlbGYpOgogICAgaWYgc2VsZi5ub2RlOgogICAgICAgIHNlbGYubm9kZS5ob2NrZXkgPSBGYWxzZQogICAgICAgIHNlbGYubm9kZS5iaWxsYm9hcmRfb3BhY2l0eSA9IDAuMAogICAgICAgIGJhLnBsYXlzb3VuZChiYS5nZXRzb3VuZCgncG93ZXJkb3duMDEnKSkKICAgICAgICAKZGVmIF9pY2VfbWFuX29mZl9mbGFzaChzZWxmKToKICAgIGlmIHNlbGYubm9kZToKICAgICAgICBmYWN0b3J5ID0gTmV3UG93ZXJ1cEJveEZhY3RvcnkuZ2V0KCkKICAgICAgICBzZWxmLm5vZGUuYmlsbGJvYXJkX3RleHR1cmUgPSBmYWN0b3J5LnRleF9pY2VfbWFuCiAgICAgICAgc2VsZi5ub2RlLmJpbGxib2FyZF9vcGFjaXR5ID0gMS4wCiAgICAgICAgc2VsZi5ub2RlLmJpbGxib2FyZF9jcm9zc19vdXQgPSBUcnVlCiAgICAgICAgCmRlZiBfaWNlX21hbl93ZWFyX29mZihzZWxmKToKICAgIGlmIHNlbGYubm9kZToKICAgICAgICBmID0gc2VsZi5jb2xvclswXQogICAgICAgIGkgPSAoMCwxLDQpCiAgICAgICAgCiAgICAgICAgYm9tYiA9IHNlbGYuYm1iX2NvbG9yWzBdICAgICAgICAKICAgICAgICBpZiBib21iICE9ICdpY2VfYnViYmxlJzogc2VsZi5ib21iX3R5cGUgPSBib21iCiAgICAgICAgZWxzZTogc2VsZi5ib21iX3R5cGUgPSAnbm9ybWFsJwogICAgICAgIAogICAgICAgIHNlbGYuZnJlZXplX3B1bmNoID0gRmFsc2UKICAgICAgICBzZWxmLm5vZGUuYmlsbGJvYXJkX29wYWNpdHkgPSAwLjAKICAgICAgICBiYS5hbmltYXRlX2FycmF5KHNlbGYubm9kZSwnY29sb3InLDMsezA6IGYsIDAuMzogaSwgMC42OiBmfSkKICAgICAgICBiYS5wbGF5c291bmQoYmEuZ2V0c291bmQoJ3Bvd2VyZG93bjAxJykpCiAgICAgICAgClNwYXouX3BtMl9zcHpfb2xkID0gU3Bhei5fX2luaXRfXwpkZWYgX2luaXRfc3Bhel8oc2VsZiwqYXJncywgKiprd2FyZ3MpOgogICAgc2VsZi5fcG0yX3Nwel9vbGQoKmFyZ3MsICoqa3dhcmdzKQogICAgc2VsZi5lZGdfZWZmID0gRmFsc2UKICAgIHNlbGYua2lsbF9lZmYgPSBGYWxzZQogICAgc2VsZi5mcmVlemVfcHVuY2ggPSBGYWxzZQogICAgc2VsZi5kaWUgPSBGYWxzZQogICAgc2VsZi5jb2xvcjogbGlzdCA9IFtdCiAgICBzZWxmLmNvbG9yLmFwcGVuZChzZWxmLm5vZGUuY29sb3IpCiAgICAKICAgIHNlbGYudGFua3NoaWVsZCA9IHsiVGFuayI6IEZhbHNlLAogICAgICAgICAgICAgICAgICAgICAgICJSZWR1Y3Rpb24iOiBGYWxzZSwKICAgICAgICAgICAgICAgICAgICAgICAiU2hpZWxkIjogTm9uZX0KClNwYXouX3N1cGVyX29uX3B1bmNoX3ByZXNzID0gU3Bhei5vbl9wdW5jaF9wcmVzcwpkZWYgc3Bhel9vbl9wdW5jaF9wcmVzcyhzZWxmKSAtPiBOb25lOgogICAgc2VsZi5fc3VwZXJfb25fcHVuY2hfcHJlc3MoKQoKICAgIGlmIHNlbGYudGFua3NoaWVsZFsnVGFuayddOgogICAgICAgIHRyeToKICAgICAgICAgICAgc2VsZi50YW5rc2hpZWxkWydSZWR1Y3Rpb24nXSA9IFRydWUKICAgICAgICAgICAgCiAgICAgICAgICAgIHNoaWVsZCA9IGJhLm5ld25vZGUoJ3NoaWVsZCcsb3duZXI9c2VsZi5ub2RlLAogICAgICAgICAgICAgICAgYXR0cnM9eydjb2xvcic6ICg0LDEsNCksJ3JhZGl1cyc6IDEuM30pCiAgICAgICAgICAgIHNlbGYubm9kZS5jb25uZWN0YXR0cigncG9zaXRpb25fY2VudGVyJywgc2hpZWxkLCAncG9zaXRpb24nKQogICAgICAgICAgICAKICAgICAgICAgICAgc2VsZi50YW5rc2hpZWxkWydTaGllbGQnXSA9IHNoaWVsZAogICAgICAgIGV4Y2VwdDogcGFzcwoKU3Bhei5fc3VwZXJfb25fcHVuY2hfcmVsZWFzZSA9IFNwYXoub25fcHVuY2hfcmVsZWFzZQpkZWYgc3Bhel9vbl9wdW5jaF9yZWxlYXNlKHNlbGYpIC0+IE5vbmU6CiAgICBzZWxmLl9zdXBlcl9vbl9wdW5jaF9yZWxlYXNlKCkKICAgIHRyeToKICAgICAgICBzZWxmLnRhbmtzaGllbGRbJ1NoaWVsZCddLmRlbGV0ZSgpCiAgICAgICAgc2VsZi50YW5rc2hpZWxkWydSZWR1Y3Rpb24nXSA9IEZhbHNlCiAgICBleGNlcHQ6IHBhc3MKCmRlZiBuZXdfZ2V0X2JvbWJfdHlwZV90ZXgoc2VsZikgLT4gYmEuVGV4dHVyZToKICAgICAgICBmYWN0b3J5ID0gTmV3UG93ZXJ1cEJveEZhY3RvcnkuZ2V0KCkKICAgICAgICBpZiBzZWxmLmJvbWJfdHlwZSA9PSAnc3RpY2t5JzoKICAgICAgICAgICAgcmV0dXJuIGZhY3RvcnkudGV4X3N0aWNreV9ib21icwogICAgICAgIGlmIHNlbGYuYm9tYl90eXBlID09ICdpY2UnOgogICAgICAgICAgICByZXR1cm4gZmFjdG9yeS50ZXhfaWNlX2JvbWJzCiAgICAgICAgaWYgc2VsZi5ib21iX3R5cGUgPT0gJ2ltcGFjdCc6CiAgICAgICAgICAgIHJldHVybiBmYWN0b3J5LnRleF9pbXBhY3RfYm9tYnMKICAgICAgICBpZiBzZWxmLmJvbWJfdHlwZSA9PSAnaW1wYWlybWVudCc6CiAgICAgICAgICAgIHJldHVybiBmYWN0b3J5LnRleF9pbXBhaXJtZW50X2JvbWJzCiAgICAgICAgaWYgc2VsZi5ib21iX3R5cGUgPT0gJ2ZpcmUnOgogICAgICAgICAgICByZXR1cm4gZmFjdG9yeS50ZXhfZmlyZV9ib21icwogICAgICAgIGlmIHNlbGYuYm9tYl90eXBlID09ICdmbHknOgogICAgICAgICAgICByZXR1cm4gZmFjdG9yeS50ZXhfZmx5X2JvbWJzCiAgICAgICAgcmV0dXJuIGZhY3RvcnkudGV4X2ltcGFjdF9ib21icwogICAgICAgICMgcmFpc2UgVmFsdWVFcnJvcignaW52YWxpZCBib21iIHR5cGUnKQoKZGVmIG5ld19oYW5kbGVtZXNzYWdlKHNlbGYsIG1zZzogQW55KSAtPiBBbnk6CiAgICBhc3NlcnQgbm90IHNlbGYuZXhwaXJlZAogICAgCiAgICBpZiBpc2luc3RhbmNlKG1zZywgYmEuUGlja2VkVXBNZXNzYWdlKToKICAgICAgICBpZiBzZWxmLm5vZGU6CiAgICAgICAgICAgIHNlbGYubm9kZS5oYW5kbGVtZXNzYWdlKCdodXJ0X3NvdW5kJykKICAgICAgICAgICAgc2VsZi5ub2RlLmhhbmRsZW1lc3NhZ2UoJ3BpY2tlZF91cCcpCgogICAgICAgIHNlbGYuX251bV90aW1lc19oaXQgKz0gMQoKICAgIGVsaWYgaXNpbnN0YW5jZShtc2csIGJhLlNob3VsZFNoYXR0ZXJNZXNzYWdlKToKICAgICAgICBiYS50aW1lcigwLjAwMSwgYmEuQ2FsbChzZWxmLnNoYXR0ZXIpKQoKICAgIGVsaWYgaXNpbnN0YW5jZShtc2csIGJhLkltcGFjdERhbWFnZU1lc3NhZ2UpOgogICAgICAgIGJhLnRpbWVyKDAuMDAxLCBiYS5DYWxsKHNlbGYuX2hpdF9zZWxmLCBtc2cuaW50ZW5zaXR5KSkKCiAgICBlbGlmIGlzaW5zdGFuY2UobXNnLCBiYS5Qb3dlcnVwTWVzc2FnZSk6CiAgICAgICAgZmFjdG9yeSA9IE5ld1Bvd2VydXBCb3hGYWN0b3J5LmdldCgpCiAgICAgICAgaWYgc2VsZi5fZGVhZCBvciBub3Qgc2VsZi5ub2RlOgogICAgICAgICAgICByZXR1cm4gVHJ1ZQogICAgICAgIGlmIHNlbGYucGlja191cF9wb3dlcnVwX2NhbGxiYWNrIGlzIG5vdCBOb25lOgogICAgICAgICAgICBzZWxmLnBpY2tfdXBfcG93ZXJ1cF9jYWxsYmFjayhzZWxmKQogICAgICAgIGlmIG1zZy5wb3dlcnVwdHlwZSA9PSAndHJpcGxlX2JvbWJzJzoKICAgICAgICAgICAgdGV4ID0gUG93ZXJ1cEJveEZhY3RvcnkuZ2V0KCkudGV4X2JvbWIKICAgICAgICAgICAgc2VsZi5fZmxhc2hfYmlsbGJvYXJkKHRleCkKICAgICAgICAgICAgc2VsZi5zZXRfYm9tYl9jb3VudCgzKQogICAgICAgICAgICBpZiBzZWxmLnBvd2VydXBzX2V4cGlyZToKICAgICAgICAgICAgICAgIHNlbGYubm9kZS5taW5pX2JpbGxib2FyZF8xX3RleHR1cmUgPSB0ZXgKICAgICAgICAgICAgICAgIHRfbXMgPSBiYS50aW1lKHRpbWVmb3JtYXQ9YmEuVGltZUZvcm1hdC5NSUxMSVNFQ09ORFMpCiAgICAgICAgICAgICAgICBhc3NlcnQgaXNpbnN0YW5jZSh0X21zLCBpbnQpCiAgICAgICAgICAgICAgICBzZWxmLm5vZGUubWluaV9iaWxsYm9hcmRfMV9zdGFydF90aW1lID0gdF9tcwogICAgICAgICAgICAgICAgc2VsZi5ub2RlLm1pbmlfYmlsbGJvYXJkXzFfZW5kX3RpbWUgPSAoCiAgICAgICAgICAgICAgICAgICAgdF9tcyArIFBPV0VSVVBfV0VBUl9PRkZfVElNRSkKICAgICAgICAgICAgICAgIHNlbGYuX211bHRpX2JvbWJfd2Vhcl9vZmZfdGltZXIgPSAoYmEuVGltZXIoCiAgICAgICAgICAgICAgICAgICAgKFBPV0VSVVBfV0VBUl9PRkZfVElNRSAtIDIwMDApLAogICAgICAgICAgICAgICAgICAgIGJhLkNhbGwoc2VsZi5fbXVsdGlfYm9tYl93ZWFyX29mZl9mbGFzaCksCiAgICAgICAgICAgICAgICAgICAgdGltZWZvcm1hdD1iYS5UaW1lRm9ybWF0Lk1JTExJU0VDT05EUykpCiAgICAgICAgICAgICAgICBzZWxmLl9tdWx0aV9ib21iX3dlYXJfb2ZmX3RpbWVyID0gKGJhLlRpbWVyKAogICAgICAgICAgICAgICAgICAgIFBPV0VSVVBfV0VBUl9PRkZfVElNRSwKICAgICAgICAgICAgICAgICAgICBiYS5DYWxsKHNlbGYuX211bHRpX2JvbWJfd2Vhcl9vZmYpLAogICAgICAgICAgICAgICAgICAgIHRpbWVmb3JtYXQ9YmEuVGltZUZvcm1hdC5NSUxMSVNFQ09ORFMpKQogICAgICAgIGVsaWYgbXNnLnBvd2VydXB0eXBlID09ICdsYW5kX21pbmVzJzoKICAgICAgICAgICAgc2VsZi5zZXRfbGFuZF9taW5lX2NvdW50KG1pbihzZWxmLmxhbmRfbWluZV9jb3VudCArIDMsIDMpKQogICAgICAgIGVsaWYgbXNnLnBvd2VydXB0eXBlID09ICdpbXBhY3RfYm9tYnMnOgogICAgICAgICAgICBzZWxmLmJvbWJfdHlwZSA9ICdpbXBhY3QnCiAgICAgICAgICAgIHRleCA9IHNlbGYuX2dldF9ib21iX3R5cGVfdGV4KCkKICAgICAgICAgICAgc2VsZi5fZmxhc2hfYmlsbGJvYXJkKHRleCkKICAgICAgICAgICAgaWYgc2VsZi5wb3dlcnVwc19leHBpcmU6CiAgICAgICAgICAgICAgICBzZWxmLm5vZGUubWluaV9iaWxsYm9hcmRfMl90ZXh0dXJlID0gdGV4CiAgICAgICAgICAgICAgICB0X21zID0gYmEudGltZSh0aW1lZm9ybWF0PWJhLlRpbWVGb3JtYXQuTUlMTElTRUNPTkRTKQogICAgICAgICAgICAgICAgYXNzZXJ0IGlzaW5zdGFuY2UodF9tcywgaW50KQogICAgICAgICAgICAgICAgc2VsZi5ub2RlLm1pbmlfYmlsbGJvYXJkXzJfc3RhcnRfdGltZSA9IHRfbXMKICAgICAgICAgICAgICAgIHNlbGYubm9kZS5taW5pX2JpbGxib2FyZF8yX2VuZF90aW1lID0gKAogICAgICAgICAgICAgICAgICAgIHRfbXMgKyBQT1dFUlVQX1dFQVJfT0ZGX1RJTUUpCiAgICAgICAgICAgICAgICBzZWxmLl9ib21iX3dlYXJfb2ZmX2ZsYXNoX3RpbWVyID0gKGJhLlRpbWVyKAogICAgICAgICAgICAgICAgICAgIFBPV0VSVVBfV0VBUl9PRkZfVElNRSAtIDIwMDAsCiAgICAgICAgICAgICAgICAgICAgYmEuQ2FsbChzZWxmLl9ib21iX3dlYXJfb2ZmX2ZsYXNoKSwKICAgICAgICAgICAgICAgICAgICB0aW1lZm9ybWF0PWJhLlRpbWVGb3JtYXQuTUlMTElTRUNPTkRTKSkKICAgICAgICAgICAgICAgIHNlbGYuX2JvbWJfd2Vhcl9vZmZfdGltZXIgPSAoYmEuVGltZXIoCiAgICAgICAgICAgICAgICAgICAgUE9XRVJVUF9XRUFSX09GRl9USU1FLAogICAgICAgICAgICAgICAgICAgIGJhLkNhbGwoc2VsZi5fYm9tYl93ZWFyX29mZiksCiAgICAgICAgICAgICAgICAgICAgdGltZWZvcm1hdD1iYS5UaW1lRm9ybWF0Lk1JTExJU0VDT05EUykpCiAgICAgICAgZWxpZiBtc2cucG93ZXJ1cHR5cGUgPT0gJ3N0aWNreV9ib21icyc6CiAgICAgICAgICAgIHNlbGYuYm9tYl90eXBlID0gJ3N0aWNreScKICAgICAgICAgICAgdGV4ID0gc2VsZi5fZ2V0X2JvbWJfdHlwZV90ZXgoKQogICAgICAgICAgICBzZWxmLl9mbGFzaF9iaWxsYm9hcmQodGV4KQogICAgICAgICAgICBpZiBzZWxmLnBvd2VydXBzX2V4cGlyZToKICAgICAgICAgICAgICAgIHNlbGYubm9kZS5taW5pX2JpbGxib2FyZF8yX3RleHR1cmUgPSB0ZXgKICAgICAgICAgICAgICAgIHRfbXMgPSBiYS50aW1lKHRpbWVmb3JtYXQ9YmEuVGltZUZvcm1hdC5NSUxMSVNFQ09ORFMpCiAgICAgICAgICAgICAgICBhc3NlcnQgaXNpbnN0YW5jZSh0X21zLCBpbnQpCiAgICAgICAgICAgICAgICBzZWxmLm5vZGUubWluaV9iaWxsYm9hcmRfMl9zdGFydF90aW1lID0gdF9tcwogICAgICAgICAgICAgICAgc2VsZi5ub2RlLm1pbmlfYmlsbGJvYXJkXzJfZW5kX3RpbWUgPSAoCiAgICAgICAgICAgICAgICAgICAgdF9tcyArIFBPV0VSVVBfV0VBUl9PRkZfVElNRSkKICAgICAgICAgICAgICAgIHNlbGYuX2JvbWJfd2Vhcl9vZmZfZmxhc2hfdGltZXIgPSAoYmEuVGltZXIoCiAgICAgICAgICAgICAgICAgICAgUE9XRVJVUF9XRUFSX09GRl9USU1FIC0gMjAwMCwKICAgICAgICAgICAgICAgICAgICBiYS5DYWxsKHNlbGYuX2JvbWJfd2Vhcl9vZmZfZmxhc2gpLAogICAgICAgICAgICAgICAgICAgIHRpbWVmb3JtYXQ9YmEuVGltZUZvcm1hdC5NSUxMSVNFQ09ORFMpKQogICAgICAgICAgICAgICAgc2VsZi5fYm9tYl93ZWFyX29mZl90aW1lciA9IChiYS5UaW1lcigKICAgICAgICAgICAgICAgICAgICBQT1dFUlVQX1dFQVJfT0ZGX1RJTUUsCiAgICAgICAgICAgICAgICAgICAgYmEuQ2FsbChzZWxmLl9ib21iX3dlYXJfb2ZmKSwKICAgICAgICAgICAgICAgICAgICB0aW1lZm9ybWF0PWJhLlRpbWVGb3JtYXQuTUlMTElTRUNPTkRTKSkKICAgICAgICBlbGlmIG1zZy5wb3dlcnVwdHlwZSA9PSAncHVuY2gnOgogICAgICAgICAgICBzZWxmLl9oYXNfYm94aW5nX2dsb3ZlcyA9IFRydWUKICAgICAgICAgICAgdGV4ID0gUG93ZXJ1cEJveEZhY3RvcnkuZ2V0KCkudGV4X3B1bmNoCiAgICAgICAgICAgIHNlbGYuX2ZsYXNoX2JpbGxib2FyZCh0ZXgpCiAgICAgICAgICAgIHNlbGYuZXF1aXBfYm94aW5nX2dsb3ZlcygpCiAgICAgICAgICAgIGlmIHNlbGYucG93ZXJ1cHNfZXhwaXJlOgogICAgICAgICAgICAgICAgc2VsZi5ub2RlLmJveGluZ19nbG92ZXNfZmxhc2hpbmcgPSBGYWxzZQogICAgICAgICAgICAgICAgc2VsZi5ub2RlLm1pbmlfYmlsbGJvYXJkXzNfdGV4dHVyZSA9IHRleAogICAgICAgICAgICAgICAgdF9tcyA9IGJhLnRpbWUodGltZWZvcm1hdD1iYS5UaW1lRm9ybWF0Lk1JTExJU0VDT05EUykKICAgICAgICAgICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHRfbXMsIGludCkKICAgICAgICAgICAgICAgIHNlbGYubm9kZS5taW5pX2JpbGxib2FyZF8zX3N0YXJ0X3RpbWUgPSB0X21zCiAgICAgICAgICAgICAgICBzZWxmLm5vZGUubWluaV9iaWxsYm9hcmRfM19lbmRfdGltZSA9ICgKICAgICAgICAgICAgICAgICAgICB0X21zICsgUE9XRVJVUF9XRUFSX09GRl9USU1FKQogICAgICAgICAgICAgICAgc2VsZi5fYm94aW5nX2dsb3Zlc193ZWFyX29mZl9mbGFzaF90aW1lciA9IChiYS5UaW1lcigKICAgICAgICAgICAgICAgICAgICBQT1dFUlVQX1dFQVJfT0ZGX1RJTUUgLSAyMDAwLAogICAgICAgICAgICAgICAgICAgIGJhLldlYWtDYWxsKHNlbGYuX2dsb3Zlc193ZWFyX29mZl9mbGFzaCksCiAgICAgICAgICAgICAgICAgICAgdGltZWZvcm1hdD1iYS5UaW1lRm9ybWF0Lk1JTExJU0VDT05EUykpCiAgICAgICAgICAgICAgICBzZWxmLl9ib3hpbmdfZ2xvdmVzX3dlYXJfb2ZmX3RpbWVyID0gKGJhLlRpbWVyKAogICAgICAgICAgICAgICAgICAgIFBPV0VSVVBfV0VBUl9PRkZfVElNRSwKICAgICAgICAgICAgICAgICAgICBiYS5XZWFrQ2FsbChzZWxmLl9nbG92ZXNfd2Vhcl9vZmYpLAogICAgICAgICAgICAgICAgICAgIHRpbWVmb3JtYXQ9YmEuVGltZUZvcm1hdC5NSUxMSVNFQ09ORFMpKQogICAgICAgIGVsaWYgbXNnLnBvd2VydXB0eXBlID09ICdzaGllbGQnOgogICAgICAgICAgICBmYWN0b3J5ID0gU3BhekZhY3RvcnkuZ2V0KCkKICAgICAgICAgICAgc2VsZi5lcXVpcF9zaGllbGRzKGRlY2F5PWZhY3Rvcnkuc2hpZWxkX2RlY2F5X3JhdGUgPiAwKQogICAgICAgIGVsaWYgbXNnLnBvd2VydXB0eXBlID09ICdjdXJzZSc6CiAgICAgICAgICAgIHNlbGYuY3Vyc2UoKQogICAgICAgIGVsaWYgbXNnLnBvd2VydXB0eXBlID09ICdpY2VfYm9tYnMnOgogICAgICAgICAgICBzZWxmLmJvbWJfdHlwZSA9ICdpY2UnCiAgICAgICAgICAgIHRleCA9IHNlbGYuX2dldF9ib21iX3R5cGVfdGV4KCkKICAgICAgICAgICAgc2VsZi5fZmxhc2hfYmlsbGJvYXJkKHRleCkKICAgICAgICAgICAgaWYgc2VsZi5wb3dlcnVwc19leHBpcmU6CiAgICAgICAgICAgICAgICBzZWxmLm5vZGUubWluaV9iaWxsYm9hcmRfMl90ZXh0dXJlID0gdGV4CiAgICAgICAgICAgICAgICB0X21zID0gYmEudGltZSh0aW1lZm9ybWF0PWJhLlRpbWVGb3JtYXQuTUlMTElTRUNPTkRTKQogICAgICAgICAgICAgICAgYXNzZXJ0IGlzaW5zdGFuY2UodF9tcywgaW50KQogICAgICAgICAgICAgICAgc2VsZi5ub2RlLm1pbmlfYmlsbGJvYXJkXzJfc3RhcnRfdGltZSA9IHRfbXMKICAgICAgICAgICAgICAgIHNlbGYubm9kZS5taW5pX2JpbGxib2FyZF8yX2VuZF90aW1lID0gKAogICAgICAgICAgICAgICAgICAgIHRfbXMgKyBQT1dFUlVQX1dFQVJfT0ZGX1RJTUUpCiAgICAgICAgICAgICAgICBzZWxmLl9ib21iX3dlYXJfb2ZmX2ZsYXNoX3RpbWVyID0gKGJhLlRpbWVyKAogICAgICAgICAgICAgICAgICAgIFBPV0VSVVBfV0VBUl9PRkZfVElNRSAtIDIwMDAsCiAgICAgICAgICAgICAgICAgICAgYmEuV2Vha0NhbGwoc2VsZi5fYm9tYl93ZWFyX29mZl9mbGFzaCksCiAgICAgICAgICAgICAgICAgICAgdGltZWZvcm1hdD1iYS5UaW1lRm9ybWF0Lk1JTExJU0VDT05EUykpCiAgICAgICAgICAgICAgICBzZWxmLl9ib21iX3dlYXJfb2ZmX3RpbWVyID0gKGJhLlRpbWVyKAogICAgICAgICAgICAgICAgICAgIFBPV0VSVVBfV0VBUl9PRkZfVElNRSwKICAgICAgICAgICAgICAgICAgICBiYS5XZWFrQ2FsbChzZWxmLl9ib21iX3dlYXJfb2ZmKSwKICAgICAgICAgICAgICAgICAgICB0aW1lZm9ybWF0PWJhLlRpbWVGb3JtYXQuTUlMTElTRUNPTkRTKSkKICAgICAgICBlbGlmIG1zZy5wb3dlcnVwdHlwZSA9PSAnaGVhbHRoJzoKICAgICAgICAgICAgaWYgc2VsZi5lZGdfZWZmOgogICAgICAgICAgICAgICAgZiA9IHNlbGYuY29sb3JbMF0KICAgICAgICAgICAgICAgIHIgPSAoMiwwLDApCiAgICAgICAgICAgICAgICBnID0gKDAsMiwwKQogICAgICAgICAgICAgICAgYmEuYW5pbWF0ZV9hcnJheShzZWxmLm5vZGUsJ2NvbG9yJywzLHswOiByLCAwLjY6IGcsIDEuMDogZn0pCiAgICAgICAgICAgICAgICBzZWxmLmVkZ19lZmYgPSBGYWxzZQogICAgICAgICAgICBpZiBzZWxmLl9jdXJzZWQ6CiAgICAgICAgICAgICAgICBzZWxmLl9jdXJzZWQgPSBGYWxzZQogICAgICAgICAgICAgICAgZmFjdG9yeSA9IFNwYXpGYWN0b3J5LmdldCgpCiAgICAgICAgICAgICAgICBmb3IgYXR0ciBpbiBbJ21hdGVyaWFscycsICdyb2xsZXJfbWF0ZXJpYWxzJ106CiAgICAgICAgICAgICAgICAgICAgbWF0ZXJpYWxzID0gZ2V0YXR0cihzZWxmLm5vZGUsIGF0dHIpCiAgICAgICAgICAgICAgICAgICAgaWYgZmFjdG9yeS5jdXJzZV9tYXRlcmlhbCBpbiBtYXRlcmlhbHM6CiAgICAgICAgICAgICAgICAgICAgICAgIHNldGF0dHIoCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBzZWxmLm5vZGUsIGF0dHIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICB0dXBsZShtIGZvciBtIGluIG1hdGVyaWFscwogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaWYgbSAhPSBmYWN0b3J5LmN1cnNlX21hdGVyaWFsKSkKICAgICAgICAgICAgICAgIHNlbGYubm9kZS5jdXJzZV9kZWF0aF90aW1lID0gMAogICAgICAgICAgICBzZWxmLmhpdHBvaW50cyA9IHNlbGYuaGl0cG9pbnRzX21heAogICAgICAgICAgICBzZWxmLl9mbGFzaF9iaWxsYm9hcmQoUG93ZXJ1cEJveEZhY3RvcnkuZ2V0KCkudGV4X2hlYWx0aCkKICAgICAgICAgICAgc2VsZi5ub2RlLmh1cnQgPSAwCiAgICAgICAgICAgIHNlbGYuX2xhc3RfaGl0X3RpbWUgPSBOb25lCiAgICAgICAgICAgIHNlbGYuX251bV90aW1lc19oaXQgPSAwCgogICAgICAgIGVsaWYgbXNnLnBvd2VydXB0eXBlID09ICd0YW5rX3NoaWVsZCc6CiAgICAgICAgICAgIHNlbGYudGFua3NoaWVsZFsnVGFuayddID0gVHJ1ZQogICAgICAgICAgICBzZWxmLmVkZ19lZmYgPSBGYWxzZQogICAgICAgICAgICB0ZXggPSBmYWN0b3J5LnRleF90YW5rX3NoaWVsZAogICAgICAgICAgICBzZWxmLl9mbGFzaF9iaWxsYm9hcmQodGV4KQoKICAgICAgICBlbGlmIG1zZy5wb3dlcnVwdHlwZSA9PSAnaGVhbHRoX2RhbWFnZSc6CiAgICAgICAgICAgIHRleCA9IGZhY3RvcnkudGV4X2hlYWx0aF9kYW1hZ2UKICAgICAgICAgICAgc2VsZi5lZGdfZWZmID0gVHJ1ZQogICAgICAgICAgICBmID0gc2VsZi5jb2xvclswXQogICAgICAgICAgICBpID0gKDIsMC41LDIpCiAgICAgICAgICAgIGJhLmFuaW1hdGVfYXJyYXkoc2VsZi5ub2RlLCdjb2xvcicsMyx7MDogaSwgMC41OiBpLCAwLjY6IGZ9KQogICAgICAgICAgICBzZWxmLl9mbGFzaF9iaWxsYm9hcmQodGV4KQogICAgICAgICAgICBzZWxmLnRhbmtzaGllbGRbJ1RhbmsnXSA9IEZhbHNlCiAgICAgICAgICAgIHNlbGYuZnJlZXplX3B1bmNoID0gRmFsc2UKCiAgICAgICAgZWxpZiBtc2cucG93ZXJ1cHR5cGUgPT0gJ2dvb2RieWUnOgogICAgICAgICAgICB0ZXggPSBmYWN0b3J5LnRleF9nb29kYnllCiAgICAgICAgICAgIHNlbGYuX2ZsYXNoX2JpbGxib2FyZCh0ZXgpCiAgICAgICAgICAgIHNlbGYua2lsbF9lZmYgPSBUcnVlCgogICAgICAgIGVsaWYgbXNnLnBvd2VydXB0eXBlID09ICdmbHlfYm9tYnMnOgogICAgICAgICAgICBzZWxmLmJvbWJfdHlwZSA9ICdmbHknCiAgICAgICAgICAgIHRleCA9IHNlbGYuX2dldF9ib21iX3R5cGVfdGV4KCkKICAgICAgICAgICAgc2VsZi5fZmxhc2hfYmlsbGJvYXJkKHRleCkKICAgICAgICAgICAgaWYgc2VsZi5wb3dlcnVwc19leHBpcmU6CiAgICAgICAgICAgICAgICBzZWxmLm5vZGUubWluaV9iaWxsYm9hcmRfMl90ZXh0dXJlID0gdGV4CiAgICAgICAgICAgICAgICB0X21zID0gYmEudGltZSh0aW1lZm9ybWF0PWJhLlRpbWVGb3JtYXQuTUlMTElTRUNPTkRTKQogICAgICAgICAgICAgICAgYXNzZXJ0IGlzaW5zdGFuY2UodF9tcywgaW50KQogICAgICAgICAgICAgICAgc2VsZi5ub2RlLm1pbmlfYmlsbGJvYXJkXzJfc3RhcnRfdGltZSA9IHRfbXMKICAgICAgICAgICAgICAgIHNlbGYubm9kZS5taW5pX2JpbGxib2FyZF8yX2VuZF90aW1lID0gKAogICAgICAgICAgICAgICAgICAgIHRfbXMgKyBQT1dFUlVQX1dFQVJfT0ZGX1RJTUUpCiAgICAgICAgICAgICAgICBzZWxmLl9ib21iX3dlYXJfb2ZmX2ZsYXNoX3RpbWVyID0gKGJhLlRpbWVyKAogICAgICAgICAgICAgICAgICAgIFBPV0VSVVBfV0VBUl9PRkZfVElNRSAtIDIwMDAsCiAgICAgICAgICAgICAgICAgICAgYmEuV2Vha0NhbGwoc2VsZi5fYm9tYl93ZWFyX29mZl9mbGFzaCksCiAgICAgICAgICAgICAgICAgICAgdGltZWZvcm1hdD1iYS5UaW1lRm9ybWF0Lk1JTExJU0VDT05EUykpCiAgICAgICAgICAgICAgICBzZWxmLl9ib21iX3dlYXJfb2ZmX3RpbWVyID0gKGJhLlRpbWVyKAogICAgICAgICAgICAgICAgICAgIFBPV0VSVVBfV0VBUl9PRkZfVElNRSwKICAgICAgICAgICAgICAgICAgICBiYS5XZWFrQ2FsbChzZWxmLl9ib21iX3dlYXJfb2ZmKSwKICAgICAgICAgICAgICAgICAgICB0aW1lZm9ybWF0PWJhLlRpbWVGb3JtYXQuTUlMTElTRUNPTkRTKSkKCiAgICAgICAgZWxpZiBtc2cucG93ZXJ1cHR5cGUgPT0gJ2ZpcmVfYm9tYnMnOgogICAgICAgICAgICBzZWxmLmJvbWJfdHlwZSA9ICdmaXJlJwogICAgICAgICAgICB0ZXggPSBzZWxmLl9nZXRfYm9tYl90eXBlX3RleCgpCiAgICAgICAgICAgIHNlbGYuX2ZsYXNoX2JpbGxib2FyZCh0ZXgpCiAgICAgICAgICAgIGlmIHNlbGYucG93ZXJ1cHNfZXhwaXJlOgogICAgICAgICAgICAgICAgc2VsZi5ub2RlLm1pbmlfYmlsbGJvYXJkXzJfdGV4dHVyZSA9IHRleAogICAgICAgICAgICAgICAgdF9tcyA9IGJhLnRpbWUodGltZWZvcm1hdD1iYS5UaW1lRm9ybWF0Lk1JTExJU0VDT05EUykKICAgICAgICAgICAgICAgIGFzc2VydCBpc2luc3RhbmNlKHRfbXMsIGludCkKICAgICAgICAgICAgICAgIHNlbGYubm9kZS5taW5pX2JpbGxib2FyZF8yX3N0YXJ0X3RpbWUgPSB0X21zCiAgICAgICAgICAgICAgICBzZWxmLm5vZGUubWluaV9iaWxsYm9hcmRfMl9lbmRfdGltZSA9ICgKICAgICAgICAgICAgICAgICAgICB0X21zICsgUE9XRVJVUF9XRUFSX09GRl9USU1FKQogICAgICAgICAgICAgICAgc2VsZi5fYm9tYl93ZWFyX29mZl9mbGFzaF90aW1lciA9IChiYS5UaW1lcigKICAgICAgICAgICAgICAgICAgICBQT1dFUlVQX1dFQVJfT0ZGX1RJTUUgLSAyMDAwLAogICAgICAgICAgICAgICAgICAgIGJhLldlYWtDYWxsKHNlbGYuX2JvbWJfd2Vhcl9vZmZfZmxhc2gpLAogICAgICAgICAgICAgICAgICAgIHRpbWVmb3JtYXQ9YmEuVGltZUZvcm1hdC5NSUxMSVNFQ09ORFMpKQogICAgICAgICAgICAgICAgc2VsZi5fYm9tYl93ZWFyX29mZl90aW1lciA9IChiYS5UaW1lcigKICAgICAgICAgICAgICAgICAgICBQT1dFUlVQX1dFQVJfT0ZGX1RJTUUsCiAgICAgICAgICAgICAgICAgICAgYmEuV2Vha0NhbGwoc2VsZi5fYm9tYl93ZWFyX29mZiksCiAgICAgICAgICAgICAgICAgICAgdGltZWZvcm1hdD1iYS5UaW1lRm9ybWF0Lk1JTExJU0VDT05EUykpCgogICAgICAgIGVsaWYgbXNnLnBvd2VydXB0eXBlID09ICdpbXBhaXJtZW50X2JvbWJzJzoKICAgICAgICAgICAgc2VsZi5ib21iX3R5cGUgPSAnaW1wYWlybWVudCcKICAgICAgICAgICAgdGV4ID0gc2VsZi5fZ2V0X2JvbWJfdHlwZV90ZXgoKQogICAgICAgICAgICBzZWxmLl9mbGFzaF9iaWxsYm9hcmQodGV4KQogICAgICAgICAgICBpZiBzZWxmLnBvd2VydXBzX2V4cGlyZToKICAgICAgICAgICAgICAgIHNlbGYubm9kZS5taW5pX2JpbGxib2FyZF8yX3RleHR1cmUgPSB0ZXgKICAgICAgICAgICAgICAgIHRfbXMgPSBiYS50aW1lKHRpbWVmb3JtYXQ9YmEuVGltZUZvcm1hdC5NSUxMSVNFQ09ORFMpCiAgICAgICAgICAgICAgICBhc3NlcnQgaXNpbnN0YW5jZSh0X21zLCBpbnQpCiAgICAgICAgICAgICAgICBzZWxmLm5vZGUubWluaV9iaWxsYm9hcmRfMl9zdGFydF90aW1lID0gdF9tcwogICAgICAgICAgICAgICAgc2VsZi5ub2RlLm1pbmlfYmlsbGJvYXJkXzJfZW5kX3RpbWUgPSAoCiAgICAgICAgICAgICAgICAgICAgdF9tcyArIFBPV0VSVVBfV0VBUl9PRkZfVElNRSkKICAgICAgICAgICAgICAgIHNlbGYuX2JvbWJfd2Vhcl9vZmZfZmxhc2hfdGltZXIgPSAoYmEuVGltZXIoCiAgICAgICAgICAgICAgICAgICAgUE9XRVJVUF9XRUFSX09GRl9USU1FIC0gMjAwMCwKICAgICAgICAgICAgICAgICAgICBiYS5XZWFrQ2FsbChzZWxmLl9ib21iX3dlYXJfb2ZmX2ZsYXNoKSwKICAgICAgICAgICAgICAgICAgICB0aW1lZm9ybWF0PWJhLlRpbWVGb3JtYXQuTUlMTElTRUNPTkRTKSkKICAgICAgICAgICAgICAgIHNlbGYuX2JvbWJfd2Vhcl9vZmZfdGltZXIgPSAoYmEuVGltZXIoCiAgICAgICAgICAgICAgICAgICAgUE9XRVJVUF9XRUFSX09GRl9USU1FLAogICAgICAgICAgICAgICAgICAgIGJhLldlYWtDYWxsKHNlbGYuX2JvbWJfd2Vhcl9vZmYpLAogICAgICAgICAgICAgICAgICAgIHRpbWVmb3JtYXQ9YmEuVGltZUZvcm1hdC5NSUxMSVNFQ09ORFMpKQoKICAgICAgICBlbGlmIG1zZy5wb3dlcnVwdHlwZSA9PSAnaWNlX21hbic6CiAgICAgICAgICAgIHRleCA9IGZhY3RvcnkudGV4X2ljZV9tYW4KICAgICAgICAgICAgc2VsZi5ib21iX3R5cGUgPSAnaWNlX2J1YmJsZScKICAgICAgICAgICAgc2VsZi5mcmVlemVfcHVuY2ggPSBUcnVlCiAgICAgICAgICAgIHNlbGYuZWRnX2VmZiA9IEZhbHNlCiAgICAgICAgICAgIHNlbGYubm9kZS5jb2xvciA9ICgwLDEsNCkKICAgICAgICAgICAgc2VsZi5fZmxhc2hfYmlsbGJvYXJkKHRleCkKICAgICAgICAgICAgCiAgICAgICAgICAgIGlmIHNlbGYucG93ZXJ1cHNfZXhwaXJlOgogICAgICAgICAgICAgICAgaWNlX21hbl90aW1lID0gMTcwMDAKICAgICAgICAgICAgICAgIHNlbGYubm9kZS5taW5pX2JpbGxib2FyZF8yX3RleHR1cmUgPSB0ZXgKICAgICAgICAgICAgICAgIHRfbXMgPSBiYS50aW1lKHRpbWVmb3JtYXQ9YmEuVGltZUZvcm1hdC5NSUxMSVNFQ09ORFMpCiAgICAgICAgICAgICAgICBhc3NlcnQgaXNpbnN0YW5jZSh0X21zLCBpbnQpCiAgICAgICAgICAgICAgICBzZWxmLm5vZGUubWluaV9iaWxsYm9hcmRfMl9zdGFydF90aW1lID0gdF9tcwogICAgICAgICAgICAgICAgc2VsZi5ub2RlLm1pbmlfYmlsbGJvYXJkXzJfZW5kX3RpbWUgPSAodF9tcyArIGljZV9tYW5fdGltZSkKICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgc2VsZi5pY2VfbWFuX2ZsYXNoX3RpbWVyID0gKGJhLlRpbWVyKAogICAgICAgICAgICAgICAgICAgIGljZV9tYW5fdGltZSAtIDIwMDAsCiAgICAgICAgICAgICAgICAgICAgYmEuQ2FsbChfaWNlX21hbl9vZmZfZmxhc2gsc2VsZiksCiAgICAgICAgICAgICAgICAgICAgdGltZWZvcm1hdD1iYS5UaW1lRm9ybWF0Lk1JTExJU0VDT05EUykpCiAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICBzZWxmLmljZV9tYW5fdGltZXIgPSAoYmEuVGltZXIoaWNlX21hbl90aW1lLAogICAgICAgICAgICAgICAgICAgIGJhLkNhbGwoX2ljZV9tYW5fd2Vhcl9vZmYsc2VsZiksCiAgICAgICAgICAgICAgICAgICAgdGltZWZvcm1hdD1iYS5UaW1lRm9ybWF0Lk1JTExJU0VDT05EUykpCgogICAgICAgIGVsaWYgbXNnLnBvd2VydXB0eXBlID09ICdzcGVlZCc6CiAgICAgICAgICAgIHNlbGYubm9kZS5ob2NrZXkgPSBUcnVlCiAgICAgICAgICAgIHRleCA9IGZhY3RvcnkudGV4X3NwZWVkCiAgICAgICAgICAgIHNlbGYuX2ZsYXNoX2JpbGxib2FyZCh0ZXgpCiAgICAgICAgICAgIGlmIHNlbGYucG93ZXJ1cHNfZXhwaXJlOgogICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICBzcGVlZF90aW1lID0gMTUwMDAKICAgICAgICAgICAgICAgIHNlbGYubm9kZS5taW5pX2JpbGxib2FyZF8yX3RleHR1cmUgPSB0ZXgKICAgICAgICAgICAgICAgIHRfbXMgPSBiYS50aW1lKHRpbWVmb3JtYXQ9YmEuVGltZUZvcm1hdC5NSUxMSVNFQ09ORFMpCiAgICAgICAgICAgICAgICBhc3NlcnQgaXNpbnN0YW5jZSh0X21zLCBpbnQpCiAgICAgICAgICAgICAgICBzZWxmLm5vZGUubWluaV9iaWxsYm9hcmRfMl9zdGFydF90aW1lID0gdF9tcwogICAgICAgICAgICAgICAgc2VsZi5ub2RlLm1pbmlfYmlsbGJvYXJkXzJfZW5kX3RpbWUgPSAodF9tcyArIHNwZWVkX3RpbWUpCiAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgIHNlbGYuc3BlZWRfZmxhc2hfdGltZXIgPSAoYmEuVGltZXIoCiAgICAgICAgICAgICAgICAgICAgc3BlZWRfdGltZSAtIDIwMDAsCiAgICAgICAgICAgICAgICAgICAgYmEuQ2FsbChfc3BlZWRfb2ZmX2ZsYXNoLHNlbGYpLAogICAgICAgICAgICAgICAgICAgIHRpbWVmb3JtYXQ9YmEuVGltZUZvcm1hdC5NSUxMSVNFQ09ORFMpKQogICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgc2VsZi5zcGVlZF90aW1lciA9IChiYS5UaW1lcihzcGVlZF90aW1lLAogICAgICAgICAgICAgICAgICAgIGJhLkNhbGwoX3NwZWVkX3dlYXJfb2ZmLHNlbGYpLAogICAgICAgICAgICAgICAgICAgIHRpbWVmb3JtYXQ9YmEuVGltZUZvcm1hdC5NSUxMSVNFQ09ORFMpKQogICAgICAgIAogICAgICAgIHNlbGYuYm1iX2NvbG9yOiBsaXN0ID0gW10gICAgCiAgICAgICAgc2VsZi5ibWJfY29sb3IuYXBwZW5kKHNlbGYuYm9tYl90eXBlKQoKICAgICAgICBzZWxmLm5vZGUuaGFuZGxlbWVzc2FnZSgnZmxhc2gnKQogICAgICAgIGlmIG1zZy5zb3VyY2Vub2RlOgogICAgICAgICAgICBtc2cuc291cmNlbm9kZS5oYW5kbGVtZXNzYWdlKGJhLlBvd2VydXBBY2NlcHRNZXNzYWdlKCkpCiAgICAgICAgcmV0dXJuIFRydWUKCiAgICBlbGlmIGlzaW5zdGFuY2UobXNnLCBiYS5GcmVlemVNZXNzYWdlKToKICAgICAgICBpZiBub3Qgc2VsZi5ub2RlOgogICAgICAgICAgICByZXR1cm4gTm9uZQogICAgICAgIGlmIHNlbGYubm9kZS5pbnZpbmNpYmxlOgogICAgICAgICAgICBiYS5wbGF5c291bmQoU3BhekZhY3RvcnkuZ2V0KCkuYmxvY2tfc291bmQsCiAgICAgICAgICAgICAgICAgICAgICAgICAxLjAsCiAgICAgICAgICAgICAgICAgICAgICAgICBwb3NpdGlvbj1zZWxmLm5vZGUucG9zaXRpb24pCiAgICAgICAgICAgIHJldHVybiBOb25lCiAgICAgICAgaWYgc2VsZi5zaGllbGQ6CiAgICAgICAgICAgIHJldHVybiBOb25lCiAgICAgICAgaWYgbm90IHNlbGYuZnJvemVuOgogICAgICAgICAgICBzZWxmLmZyb3plbiA9IFRydWUKICAgICAgICAgICAgc2VsZi5ub2RlLmZyb3plbiA9IFRydWUKICAgICAgICAgICAgYmEudGltZXIoNS4wLCBiYS5DYWxsKHNlbGYuaGFuZGxlbWVzc2FnZSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBiYS5UaGF3TWVzc2FnZSgpKSkKICAgICAgICAgICAgaWYgc2VsZi5oaXRwb2ludHMgPD0gMDoKICAgICAgICAgICAgICAgIHNlbGYuc2hhdHRlcigpCiAgICAgICAgaWYgc2VsZi5mcmVlemVfcHVuY2g6CiAgICAgICAgICAgIHNlbGYuaGFuZGxlbWVzc2FnZShiYS5UaGF3TWVzc2FnZSgpKQoKICAgIGVsaWYgaXNpbnN0YW5jZShtc2csIGJhLlRoYXdNZXNzYWdlKToKICAgICAgICBpZiBzZWxmLmZyb3plbiBhbmQgbm90IHNlbGYuc2hhdHRlcmVkIGFuZCBzZWxmLm5vZGU6CiAgICAgICAgICAgIHNlbGYuZnJvemVuID0gRmFsc2UKICAgICAgICAgICAgc2VsZi5ub2RlLmZyb3plbiA9IEZhbHNlCgogICAgZWxpZiBpc2luc3RhbmNlKG1zZywgYmEuSGl0TWVzc2FnZSk6CiAgICAgICAgaWYgbm90IHNlbGYubm9kZToKICAgICAgICAgICAgcmV0dXJuIE5vbmUKICAgICAgICBpZiBzZWxmLm5vZGUuaW52aW5jaWJsZToKICAgICAgICAgICAgYmEucGxheXNvdW5kKFNwYXpGYWN0b3J5LmdldCgpLmJsb2NrX3NvdW5kLAogICAgICAgICAgICAgICAgICAgICAgICAgMS4wLAogICAgICAgICAgICAgICAgICAgICAgICAgcG9zaXRpb249c2VsZi5ub2RlLnBvc2l0aW9uKQogICAgICAgICAgICByZXR1cm4gVHJ1ZQoKICAgICAgICBsb2NhbF90aW1lID0gYmEudGltZSh0aW1lZm9ybWF0PWJhLlRpbWVGb3JtYXQuTUlMTElTRUNPTkRTKQogICAgICAgIGFzc2VydCBpc2luc3RhbmNlKGxvY2FsX3RpbWUsIGludCkKICAgICAgICBpZiAoc2VsZi5fbGFzdF9oaXRfdGltZSBpcyBOb25lCiAgICAgICAgICAgICAgICBvciBsb2NhbF90aW1lIC0gc2VsZi5fbGFzdF9oaXRfdGltZSA+IDEwMDApOgogICAgICAgICAgICBzZWxmLl9udW1fdGltZXNfaGl0ICs9IDEKICAgICAgICAgICAgc2VsZi5fbGFzdF9oaXRfdGltZSA9IGxvY2FsX3RpbWUKCiAgICAgICAgbWFnID0gbXNnLm1hZ25pdHVkZSAqIHNlbGYuaW1wYWN0X3NjYWxlCiAgICAgICAgdmVsb2NpdHlfbWFnID0gbXNnLnZlbG9jaXR5X21hZ25pdHVkZSAqIHNlbGYuaW1wYWN0X3NjYWxlICAgICAgIAogICAgICAgIGRhbWFnZV9zY2FsZSA9IDAuMjIKICAgICAgICAKICAgICAgICBkZWYgZmlyZV9lZmZlY3QoKToKICAgICAgICAgICAgaWYgbm90IHNlbGYuc2hpZWxkOgogICAgICAgICAgICAgICAgaWYgc2VsZi5ub2RlLmV4aXN0cygpOgogICAgICAgICAgICAgICAgICAgIGJhLmVtaXRmeChwb3NpdGlvbj1zZWxmLm5vZGUucG9zaXRpb24sCiAgICAgICAgICAgICAgICAgICAgc2NhbGU9Myxjb3VudD01MCoyLHNwcmVhZD0wLjMsCiAgICAgICAgICAgICAgICAgICAgY2h1bmtfdHlwZT0nc3dlYXQnKQogICAgICAgICAgICAgICAgICAgIHNlbGYubm9kZS5oYW5kbGVtZXNzYWdlKCdjZWxlYnJhdGUnLCA1NjApCiAgICAgICAgICAgICAgICBlbHNlOiBzZWxmLl9maXJlX3RpbWUgPSBOb25lCiAgICAgICAgICAgIGVsc2U6IHNlbGYuX2ZpcmVfdGltZSA9IE5vbmUKICAgICAgICAKICAgICAgICBkZWYgZmlyZSh0aW1lLCBkYW1hZ2UpOgogICAgICAgICAgICBpZiBub3Qgc2VsZi5zaGllbGQgYW5kIG5vdCBzZWxmLl9kZWFkOgogICAgICAgICAgICAgICAgc2VsZi5oaXRwb2ludHMgLT0gZGFtYWdlCiAgICAgICAgICAgICAgICBiYS5zaG93X2RhbWFnZV9jb3VudChmJy17ZGFtYWdlfUhQJywKICAgICAgICAgICAgICAgICAgICBzZWxmLm5vZGUucG9zaXRpb24sIG1zZy5mb3JjZV9kaXJlY3Rpb24pCiAgICAgICAgICAgICAgICBiYS5wbGF5c291bmQoYmEuZ2V0c291bmQoJ2Z1c2UwMScpKQogICAgICAgICAgICAKICAgICAgICAgICAgaWYgZHVyYXRpb24gIT0gdGltZToKICAgICAgICAgICAgICAgIHNlbGYuX2ZpcmVfdGltZSA9IGJhLlRpbWVyKDAuMSxiYS5DYWxsKGZpcmVfZWZmZWN0KSxyZXBlYXQ9VHJ1ZSkKICAgICAgICAgICAgZWxzZTogc2VsZi5fZmlyZV90aW1lID0gTm9uZQogICAgICAgICAgICAKICAgICAgICAgICAgaWYgc2VsZi5oaXRwb2ludHMgPCAwOgogICAgICAgICAgICAgICAgc2VsZi5ub2RlLmhhbmRsZW1lc3NhZ2UoYmEuRGllTWVzc2FnZSgpKQogICAgICAgIAogICAgICAgIGlmIG1zZy5oaXRfc3VidHlwZSA9PSAnZmx5JzoKICAgICAgICAgICAgZGFtYWdlX3NjYWxlID0gMC4wCiAgICAgICAgICAgIAogICAgICAgICAgICBpZiBzZWxmLnNoaWVsZDoKICAgICAgICAgICAgICAgIHNlbGYuc2hpZWxkX2hpdHBvaW50cyAtPSAzMDAKICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgaWYgc2VsZi5zaGllbGRfaGl0cG9pbnRzIDwgMDoKICAgICAgICAgICAgICAgICAgICBzZWxmLnNoaWVsZC5kZWxldGUoKQogICAgICAgICAgICAgICAgICAgIHNlbGYuc2hpZWxkID0gTm9uZQogICAgICAgICAgICAgICAgICAgIGJhLnBsYXlzb3VuZChTcGF6RmFjdG9yeS5nZXQoKS5zaGllbGRfZG93bl9zb3VuZCwxLjAscG9zaXRpb249c2VsZi5ub2RlLnBvc2l0aW9uKQogICAgICAgIGVsaWYgbXNnLmhpdF9zdWJ0eXBlID09ICdmaXJlJzoKICAgICAgICAgICAgaW5kZXggPSAxCiAgICAgICAgICAgIGR1cmF0aW9uID0gNQogICAgICAgICAgICBkYW1hZ2UgPSAxMDMKICAgICAgICAgICAgaWYgbm90IHNlbGYuc2hpZWxkOgogICAgICAgICAgICAgICAgZm9yIGZpcmV4IGluIHJhbmdlKGR1cmF0aW9uKToKICAgICAgICAgICAgICAgICAgICBiYS50aW1lcihpbmRleCxiYS5DYWxsKGZpcmUsaW5kZXgsZGFtYWdlKSkKICAgICAgICAgICAgICAgICAgICBzZWxmLl9maXJlX3RpbWUgPSBiYS5UaW1lcigwLjEsYmEuQ2FsbChmaXJlX2VmZmVjdCkscmVwZWF0PVRydWUpCiAgICAgICAgICAgICAgICAgICAgaW5kZXggKz0gMQogICAgICAgICAgICBlbHNlOgogICAgICAgICAgICAgICAgc2VsZi5zaGllbGRfaGl0cG9pbnRzIC09IDgwCiAgICAgICAgICAgICAgICBpZiBzZWxmLnNoaWVsZF9oaXRwb2ludHMgPCAxOgogICAgICAgICAgICAgICAgICAgIHNlbGYuc2hpZWxkLmRlbGV0ZSgpCiAgICAgICAgICAgICAgICAgICAgc2VsZi5zaGllbGQgPSBOb25lCiAgICAgICAgICAgICAgICAgICAgYmEucGxheXNvdW5kKFNwYXpGYWN0b3J5LmdldCgpLnNoaWVsZF9kb3duX3NvdW5kLDEuMCxwb3NpdGlvbj1zZWxmLm5vZGUucG9zaXRpb24pCiAgICAgICAgZWxpZiBtc2cuaGl0X3N1YnR5cGUgPT0gJ2ltcGFpcm1lbnQnOgogICAgICAgICAgICBkYW1hZ2Vfc2NhbGUgPSAwCiAgICAgICAgICAgIAogICAgICAgICAgICBpZiBzZWxmLnNoaWVsZDoKICAgICAgICAgICAgICAgIHNlbGYuc2hpZWxkLmRlbGV0ZSgpCiAgICAgICAgICAgICAgICBzZWxmLnNoaWVsZCA9IE5vbmUKICAgICAgICAgICAgICAgIGJhLnBsYXlzb3VuZChTcGF6RmFjdG9yeS5nZXQoKS5zaGllbGRfZG93bl9zb3VuZCwxLjAscG9zaXRpb249c2VsZi5ub2RlLnBvc2l0aW9uKQogICAgICAgICAgICBlbHNlOgogICAgICAgICAgICAgICAgaGl0cG9pbnRzID0gaW50KHNlbGYuaGl0cG9pbnRzKjAuODApCiAgICAgICAgICAgICAgICBzZWxmLmhpdHBvaW50cyAtPSBpbnQoaGl0cG9pbnRzKQogICAgICAgICAgICAgICAgYmEuc2hvd19kYW1hZ2VfY291bnQoKGYnLXtpbnQoaGl0cG9pbnRzLzEwKX0lJyksCiAgICAgICAgICAgICAgICAgICAgc2VsZi5ub2RlLnBvc2l0aW9uLCBtc2cuZm9yY2VfZGlyZWN0aW9uKQogICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICBpZiBzZWxmLmhpdHBvaW50cyA8IDAgb3IgaGl0cG9pbnRzIDwgOTU6CiAgICAgICAgICAgICAgICAgICAgc2VsZi5ub2RlLmhhbmRsZW1lc3NhZ2UoYmEuRGllTWVzc2FnZSgpKQoKICAgICAgICBpZiBzZWxmLnNoaWVsZDoKICAgICAgICAgICAgaWYgbXNnLmZsYXRfZGFtYWdlOgogICAgICAgICAgICAgICAgZGFtYWdlID0gbXNnLmZsYXRfZGFtYWdlICogc2VsZi5pbXBhY3Rfc2NhbGUKICAgICAgICAgICAgZWxzZToKICAgICAgICAgICAgICAgIGFzc2VydCBtc2cuZm9yY2VfZGlyZWN0aW9uIGlzIG5vdCBOb25lCiAgICAgICAgICAgICAgICBzZWxmLm5vZGUuaGFuZGxlbWVzc2FnZSgKICAgICAgICAgICAgICAgICAgICAnaW1wdWxzZScsIG1zZy5wb3NbMF0sIG1zZy5wb3NbMV0sIG1zZy5wb3NbMl0sCiAgICAgICAgICAgICAgICAgICAgbXNnLnZlbG9jaXR5WzBdLCBtc2cudmVsb2NpdHlbMV0sIG1zZy52ZWxvY2l0eVsyXSwgbWFnLAogICAgICAgICAgICAgICAgICAgIHZlbG9jaXR5X21hZywgbXNnLnJhZGl1cywgMSwgbXNnLmZvcmNlX2RpcmVjdGlvblswXSwKICAgICAgICAgICAgICAgICAgICBtc2cuZm9yY2VfZGlyZWN0aW9uWzFdLCBtc2cuZm9yY2VfZGlyZWN0aW9uWzJdKQogICAgICAgICAgICAgICAgZGFtYWdlID0gZGFtYWdlX3NjYWxlICogc2VsZi5ub2RlLmRhbWFnZQoKICAgICAgICAgICAgYXNzZXJ0IHNlbGYuc2hpZWxkX2hpdHBvaW50cyBpcyBub3QgTm9uZQogICAgICAgICAgICBzZWxmLnNoaWVsZF9oaXRwb2ludHMgLT0gaW50KGRhbWFnZSkKICAgICAgICAgICAgc2VsZi5zaGllbGQuaHVydCA9ICgKICAgICAgICAgICAgICAgIDEuMCAtCiAgICAgICAgICAgICAgICBmbG9hdChzZWxmLnNoaWVsZF9oaXRwb2ludHMpIC8gc2VsZi5zaGllbGRfaGl0cG9pbnRzX21heCkKCiAgICAgICAgICAgIG1heF9zcGlsbG92ZXIgPSBTcGF6RmFjdG9yeS5nZXQoKS5tYXhfc2hpZWxkX3NwaWxsb3Zlcl9kYW1hZ2UKICAgICAgICAgICAgaWYgc2VsZi5zaGllbGRfaGl0cG9pbnRzIDw9IDA6CgogICAgICAgICAgICAgICAgc2VsZi5zaGllbGQuZGVsZXRlKCkKICAgICAgICAgICAgICAgIHNlbGYuc2hpZWxkID0gTm9uZQogICAgICAgICAgICAgICAgYmEucGxheXNvdW5kKFNwYXpGYWN0b3J5LmdldCgpLnNoaWVsZF9kb3duX3NvdW5kLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIDEuMCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwb3NpdGlvbj1zZWxmLm5vZGUucG9zaXRpb24pCgogICAgICAgICAgICAgICAgbnBvcyA9IHNlbGYubm9kZS5wb3NpdGlvbgogICAgICAgICAgICAgICAgYmEuZW1pdGZ4KHBvc2l0aW9uPShucG9zWzBdLCBucG9zWzFdICsgMC45LCBucG9zWzJdKSwKICAgICAgICAgICAgICAgICAgICAgICAgICB2ZWxvY2l0eT1zZWxmLm5vZGUudmVsb2NpdHksCiAgICAgICAgICAgICAgICAgICAgICAgICAgY291bnQ9cmFuZG9tLnJhbmRyYW5nZSgyMCwgMzApLAogICAgICAgICAgICAgICAgICAgICAgICAgIHNjYWxlPTEuMCwKICAgICAgICAgICAgICAgICAgICAgICAgICBzcHJlYWQ9MC42LAogICAgICAgICAgICAgICAgICAgICAgICAgIGNodW5rX3R5cGU9J3NwYXJrJykKCiAgICAgICAgICAgIGVsc2U6CiAgICAgICAgICAgICAgICBiYS5wbGF5c291bmQoU3BhekZhY3RvcnkuZ2V0KCkuc2hpZWxkX2hpdF9zb3VuZCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAwLjUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcG9zaXRpb249c2VsZi5ub2RlLnBvc2l0aW9uKQoKICAgICAgICAgICAgYXNzZXJ0IG1zZy5mb3JjZV9kaXJlY3Rpb24gaXMgbm90IE5vbmUKICAgICAgICAgICAgYmEuZW1pdGZ4KHBvc2l0aW9uPW1zZy5wb3MsCiAgICAgICAgICAgICAgICAgICAgICB2ZWxvY2l0eT0obXNnLmZvcmNlX2RpcmVjdGlvblswXSAqIDEuMCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtc2cuZm9yY2VfZGlyZWN0aW9uWzFdICogMS4wLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1zZy5mb3JjZV9kaXJlY3Rpb25bMl0gKiAxLjApLAogICAgICAgICAgICAgICAgICAgICAgY291bnQ9bWluKDMwLCA1ICsgaW50KGRhbWFnZSAqIDAuMDA1KSksCiAgICAgICAgICAgICAgICAgICAgICBzY2FsZT0wLjUsCiAgICAgICAgICAgICAgICAgICAgICBzcHJlYWQ9MC4zLAogICAgICAgICAgICAgICAgICAgICAgY2h1bmtfdHlwZT0nc3BhcmsnKQoKICAgICAgICAgICAgaWYgc2VsZi5zaGllbGRfaGl0cG9pbnRzIDw9IC1tYXhfc3BpbGxvdmVyOgogICAgICAgICAgICAgICAgbGVmdG92ZXJfZGFtYWdlID0gLW1heF9zcGlsbG92ZXIgLSBzZWxmLnNoaWVsZF9oaXRwb2ludHMKICAgICAgICAgICAgICAgIHNoaWVsZF9sZWZ0b3Zlcl9yYXRpbyA9IGxlZnRvdmVyX2RhbWFnZSAvIGRhbWFnZQoKICAgICAgICAgICAgICAgIG1hZyAqPSBzaGllbGRfbGVmdG92ZXJfcmF0aW8KICAgICAgICAgICAgICAgIHZlbG9jaXR5X21hZyAqPSBzaGllbGRfbGVmdG92ZXJfcmF0aW8KICAgICAgICAgICAgZWxzZToKICAgICAgICAgICAgICAgIHJldHVybiBUcnVlCiAgICAgICAgZWxzZToKICAgICAgICAgICAgc2hpZWxkX2xlZnRvdmVyX3JhdGlvID0gMS4wCgogICAgICAgIGlmIG1zZy5mbGF0X2RhbWFnZToKICAgICAgICAgICAgZGFtYWdlID0gaW50KG1zZy5mbGF0X2RhbWFnZSAqIHNlbGYuaW1wYWN0X3NjYWxlICoKICAgICAgICAgICAgICAgICAgICAgICAgIHNoaWVsZF9sZWZ0b3Zlcl9yYXRpbykKICAgICAgICBlbHNlOgogICAgICAgICAgICBhc3NlcnQgbXNnLmZvcmNlX2RpcmVjdGlvbiBpcyBub3QgTm9uZQogICAgICAgICAgICBzZWxmLm5vZGUuaGFuZGxlbWVzc2FnZSgKICAgICAgICAgICAgICAgICdpbXB1bHNlJywgbXNnLnBvc1swXSwgbXNnLnBvc1sxXSwgbXNnLnBvc1syXSwKICAgICAgICAgICAgICAgIG1zZy52ZWxvY2l0eVswXSwgbXNnLnZlbG9jaXR5WzFdLCBtc2cudmVsb2NpdHlbMl0sIG1hZywKICAgICAgICAgICAgICAgIHZlbG9jaXR5X21hZywgbXNnLnJhZGl1cywgMCwgbXNnLmZvcmNlX2RpcmVjdGlvblswXSwKICAgICAgICAgICAgICAgIG1zZy5mb3JjZV9kaXJlY3Rpb25bMV0sIG1zZy5mb3JjZV9kaXJlY3Rpb25bMl0pCgogICAgICAgICAgICBkYW1hZ2UgPSBpbnQoZGFtYWdlX3NjYWxlICogc2VsZi5ub2RlLmRhbWFnZSkKICAgICAgICAgICAgCiAgICAgICAgaWYgc2VsZi50YW5rc2hpZWxkWydSZWR1Y3Rpb24nXToKICAgICAgICAgICAgcG9yY2VudGFqZSA9IHBlcmNlbnRhZ2VfdGFua19zaGllbGQoKQogICAgICAgICAgICBkaXNtID0gaW50KGRhbWFnZSpwb3JjZW50YWplKQogICAgICAgICAgICBkYW1hZ2UgPSBpbnQoZGFtYWdlLWRpc20pCiAgICAgICAgICAgIAogICAgICAgICAgICBiYS5zaG93X2RhbWFnZV9jb3VudCgnLScgKyBzdHIoaW50KGRhbWFnZSAvIDEwKSkgKyAnJScsCiAgICAgICAgICAgICAgICBtc2cucG9zLCBtc2cuZm9yY2VfZGlyZWN0aW9uKQoKICAgICAgICBzZWxmLm5vZGUuaGFuZGxlbWVzc2FnZSgnaHVydF9zb3VuZCcpCgogICAgICAgIGlmIHNlbGYuZWRnX2VmZjoKICAgICAgICAgICAgcG9yY2VudGFqZSA9IHBlcmNlbnRhZ2VfaGVhbHRoX2RhbWFnZSgpCiAgICAgICAgICAgIGRtZ19kaXNtID0gaW50KGRhbWFnZSpwb3JjZW50YWplKQogICAgICAgICAgICBzZWxmLmhpdHBvaW50cyArPSBkbWdfZGlzbQoKICAgICAgICAgICAgUG9wdXBUZXh0KHRleHQ9Zicre2ludChkbWdfZGlzbS8xMCl9JScsc2NhbGU9MS41LAogICAgICAgICAgICAgICAgICAgICBwb3NpdGlvbj1zZWxmLm5vZGUucG9zaXRpb24sY29sb3I9KDAsMSwwKSkuYXV0b3JldGFpbigpCiAgICAgICAgICAgIGJhLmFuaW1hdGVfYXJyYXkoc2VsZi5ub2RlLCdjb2xvcicsMyx7MDogKDAsMSwwKSwgMC4zOTogKDAsMiwwKSwgMC40OiBzZWxmLmNvbG9yWzBdfSkKICAgICAgICAgICAgYmEucGxheXNvdW5kKGJhLmdldHNvdW5kKCdoZWFsdGhQb3dlcnVwJykpCgogICAgICAgIGlmIG1zZy5oaXRfdHlwZSA9PSAncHVuY2gnOgogICAgICAgICAgICBzZWxmLm9uX3B1bmNoZWQoZGFtYWdlKQoKICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgaWYgbXNnLmdldF9zb3VyY2VfcGxheWVyKGJhLlBsYXllcikuYWN0b3IuZnJlZXplX3B1bmNoOgogICAgICAgICAgICAgICAgICAgIHNlbGYubm9kZS5jb2xvciA9ICgwLDEsNCkKICAgICAgICAgICAgICAgICAgICBiYS5wbGF5c291bmQoYmEuZ2V0c291bmQoJ2ZyZWV6ZScpKQogICAgICAgICAgICAgICAgICAgIHNlbGYubm9kZS5oYW5kbGVtZXNzYWdlKGJhLkZyZWV6ZU1lc3NhZ2UoKSkKICAgICAgICAgICAgZXhjZXB0OiBwYXNzCgogICAgICAgICAgICBpZiBkYW1hZ2UgPiAzNTA6CiAgICAgICAgICAgICAgICBhc3NlcnQgbXNnLmZvcmNlX2RpcmVjdGlvbiBpcyBub3QgTm9uZQogICAgICAgICAgICAgICAgYmEuc2hvd19kYW1hZ2VfY291bnQoJy0nICsgc3RyKGludChkYW1hZ2UgLyAxMCkpICsgJyUnLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbXNnLnBvcywgbXNnLmZvcmNlX2RpcmVjdGlvbikKCiAgICAgICAgICAgIGlmIG1zZy5oaXRfc3VidHlwZSA9PSAnc3VwZXJfcHVuY2gnOgogICAgICAgICAgICAgICAgYmEucGxheXNvdW5kKFNwYXpGYWN0b3J5LmdldCgpLnB1bmNoX3NvdW5kX3N0cm9uZ2VyLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIDEuMCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwb3NpdGlvbj1zZWxmLm5vZGUucG9zaXRpb24pCiAgICAgICAgICAgIGlmIGRhbWFnZSA+IDUwMDoKICAgICAgICAgICAgICAgIHNvdW5kcyA9IFNwYXpGYWN0b3J5LmdldCgpLnB1bmNoX3NvdW5kX3N0cm9uZwogICAgICAgICAgICAgICAgc291bmQgPSBzb3VuZHNbcmFuZG9tLnJhbmRyYW5nZShsZW4oc291bmRzKSldCiAgICAgICAgICAgIGVsc2U6CiAgICAgICAgICAgICAgICBzb3VuZCA9IFNwYXpGYWN0b3J5LmdldCgpLnB1bmNoX3NvdW5kCiAgICAgICAgICAgIGJhLnBsYXlzb3VuZChzb3VuZCwgMS4wLCBwb3NpdGlvbj1zZWxmLm5vZGUucG9zaXRpb24pCgogICAgICAgICAgICBhc3NlcnQgbXNnLmZvcmNlX2RpcmVjdGlvbiBpcyBub3QgTm9uZQogICAgICAgICAgICBiYS5lbWl0ZngocG9zaXRpb249bXNnLnBvcywKICAgICAgICAgICAgICAgICAgICAgIHZlbG9jaXR5PShtc2cuZm9yY2VfZGlyZWN0aW9uWzBdICogMC41LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1zZy5mb3JjZV9kaXJlY3Rpb25bMV0gKiAwLjUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbXNnLmZvcmNlX2RpcmVjdGlvblsyXSAqIDAuNSksCiAgICAgICAgICAgICAgICAgICAgICBjb3VudD1taW4oMTAsIDEgKyBpbnQoZGFtYWdlICogMC4wMDI1KSksCiAgICAgICAgICAgICAgICAgICAgICBzY2FsZT0wLjMsCiAgICAgICAgICAgICAgICAgICAgICBzcHJlYWQ9MC4wMykKCiAgICAgICAgICAgIGJhLmVtaXRmeChwb3NpdGlvbj1tc2cucG9zLAogICAgICAgICAgICAgICAgICAgICAgY2h1bmtfdHlwZT0nc3dlYXQnLAogICAgICAgICAgICAgICAgICAgICAgdmVsb2NpdHk9KG1zZy5mb3JjZV9kaXJlY3Rpb25bMF0gKiAxLjMsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbXNnLmZvcmNlX2RpcmVjdGlvblsxXSAqIDEuMyArIDUuMCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtc2cuZm9yY2VfZGlyZWN0aW9uWzJdICogMS4zKSwKICAgICAgICAgICAgICAgICAgICAgIGNvdW50PW1pbigzMCwgMSArIGludChkYW1hZ2UgKiAwLjA0KSksCiAgICAgICAgICAgICAgICAgICAgICBzY2FsZT0wLjksCiAgICAgICAgICAgICAgICAgICAgICBzcHJlYWQ9MC4yOCkKCiAgICAgICAgICAgIGh1cnRpbmVzcyA9IGRhbWFnZSAqIDAuMDAzCiAgICAgICAgICAgIHB1bmNocG9zID0gKG1zZy5wb3NbMF0gKyBtc2cuZm9yY2VfZGlyZWN0aW9uWzBdICogMC4wMiwKICAgICAgICAgICAgICAgICAgICAgICAgbXNnLnBvc1sxXSArIG1zZy5mb3JjZV9kaXJlY3Rpb25bMV0gKiAwLjAyLAogICAgICAgICAgICAgICAgICAgICAgICBtc2cucG9zWzJdICsgbXNnLmZvcmNlX2RpcmVjdGlvblsyXSAqIDAuMDIpCiAgICAgICAgICAgIGZsYXNoX2NvbG9yID0gKDEuMCwgMC44LCAwLjQpCiAgICAgICAgICAgIGxpZ2h0ID0gYmEubmV3bm9kZSgKICAgICAgICAgICAgICAgICdsaWdodCcsCiAgICAgICAgICAgICAgICBhdHRycz17CiAgICAgICAgICAgICAgICAgICAgJ3Bvc2l0aW9uJzogcHVuY2hwb3MsCiAgICAgICAgICAgICAgICAgICAgJ3JhZGl1cyc6IDAuMTIgKyBodXJ0aW5lc3MgKiAwLjEyLAogICAgICAgICAgICAgICAgICAgICdpbnRlbnNpdHknOiAwLjMgKiAoMS4wICsgMS4wICogaHVydGluZXNzKSwKICAgICAgICAgICAgICAgICAgICAnaGVpZ2h0X2F0dGVudWF0ZWQnOiBGYWxzZSwKICAgICAgICAgICAgICAgICAgICAnY29sb3InOiBmbGFzaF9jb2xvcgogICAgICAgICAgICAgICAgfSkKICAgICAgICAgICAgYmEudGltZXIoMC4wNiwgbGlnaHQuZGVsZXRlKQoKICAgICAgICAgICAgZmxhc2ggPSBiYS5uZXdub2RlKCdmbGFzaCcsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBhdHRycz17CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJ3Bvc2l0aW9uJzogcHVuY2hwb3MsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJ3NpemUnOiAwLjE3ICsgMC4xNyAqIGh1cnRpbmVzcywKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAnY29sb3InOiBmbGFzaF9jb2xvcgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfSkKICAgICAgICAgICAgYmEudGltZXIoMC4wNiwgZmxhc2guZGVsZXRlKQoKICAgICAgICBpZiBtc2cuaGl0X3R5cGUgPT0gJ2ltcGFjdCc6CiAgICAgICAgICAgIGFzc2VydCBtc2cuZm9yY2VfZGlyZWN0aW9uIGlzIG5vdCBOb25lCiAgICAgICAgICAgIGJhLmVtaXRmeChwb3NpdGlvbj1tc2cucG9zLAogICAgICAgICAgICAgICAgICAgICAgdmVsb2NpdHk9KG1zZy5mb3JjZV9kaXJlY3Rpb25bMF0gKiAyLjAsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbXNnLmZvcmNlX2RpcmVjdGlvblsxXSAqIDIuMCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtc2cuZm9yY2VfZGlyZWN0aW9uWzJdICogMi4wKSwKICAgICAgICAgICAgICAgICAgICAgIGNvdW50PW1pbigxMCwgMSArIGludChkYW1hZ2UgKiAwLjAxKSksCiAgICAgICAgICAgICAgICAgICAgICBzY2FsZT0wLjQsCiAgICAgICAgICAgICAgICAgICAgICBzcHJlYWQ9MC4xKQogICAgICAgIGlmIHNlbGYuaGl0cG9pbnRzID4gMDoKICAgICAgICAgICAgaWYgbXNnLmhpdF90eXBlID09ICdpbXBhY3QnIGFuZCBkYW1hZ2UgPiBzZWxmLmhpdHBvaW50czoKICAgICAgICAgICAgICAgIG5ld2RhbWFnZSA9IG1heChkYW1hZ2UgLSAyMDAsIHNlbGYuaGl0cG9pbnRzIC0gMTApCiAgICAgICAgICAgICAgICBkYW1hZ2UgPSBuZXdkYW1hZ2UKICAgICAgICAgICAgc2VsZi5ub2RlLmhhbmRsZW1lc3NhZ2UoJ2ZsYXNoJykKCiAgICAgICAgICAgIGlmIGRhbWFnZSA+IDAuMCBhbmQgc2VsZi5ub2RlLmhvbGRfbm9kZToKICAgICAgICAgICAgICAgIHNlbGYubm9kZS5ob2xkX25vZGUgPSBOb25lCiAgICAgICAgICAgIHNlbGYuaGl0cG9pbnRzIC09IGRhbWFnZQogICAgICAgICAgICBzZWxmLm5vZGUuaHVydCA9IDEuMCAtIGZsb2F0KAogICAgICAgICAgICAgICAgc2VsZi5oaXRwb2ludHMpIC8gc2VsZi5oaXRwb2ludHNfbWF4CgogICAgICAgICAgICBpZiBzZWxmLl9jdXJzZWQgYW5kIGRhbWFnZSA+IDA6CiAgICAgICAgICAgICAgICBiYS50aW1lcigKICAgICAgICAgICAgICAgICAgICAwLjA1LAogICAgICAgICAgICAgICAgICAgIGJhLkNhbGwoc2VsZi5jdXJzZV9leHBsb2RlLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1zZy5nZXRfc291cmNlX3BsYXllcihiYS5QbGF5ZXIpKSkKCiAgICAgICAgICAgIGlmIHNlbGYuZnJvemVuIGFuZCAoZGFtYWdlID4gMjAwIG9yIHNlbGYuaGl0cG9pbnRzIDw9IDApOgogICAgICAgICAgICAgICAgc2VsZi5zaGF0dGVyKCkKICAgICAgICAgICAgZWxpZiBzZWxmLmhpdHBvaW50cyA8PSAwOgogICAgICAgICAgICAgICAgc2VsZi5ub2RlLmhhbmRsZW1lc3NhZ2UoCiAgICAgICAgICAgICAgICAgICAgYmEuRGllTWVzc2FnZShob3c9YmEuRGVhdGhUeXBlLklNUEFDVCkpCgogICAgICAgIGlmIHNlbGYuaGl0cG9pbnRzIDw9IDA6CiAgICAgICAgICAgIGRhbWFnZV9hdmcgPSBzZWxmLm5vZGUuZGFtYWdlX3Ntb290aGVkICogZGFtYWdlX3NjYWxlCiAgICAgICAgICAgIGlmIGRhbWFnZV9hdmcgPiAxMDAwOgogICAgICAgICAgICAgICAgc2VsZi5zaGF0dGVyKCkKCiAgICBlbGlmIGlzaW5zdGFuY2UobXNnLCBCb21iRGllZE1lc3NhZ2UpOgogICAgICAgIHNlbGYuYm9tYl9jb3VudCArPSAxCgogICAgZWxpZiBpc2luc3RhbmNlKG1zZywgYmEuRGllTWVzc2FnZSk6CiAgICAgICAgZGVmIGRyb3BfYm9tYigpOgogICAgICAgICAgICBmb3IgeGJvbWIgaW4gcmFuZ2UoMyk6CiAgICAgICAgICAgICAgICBwID0gc2VsZi5ub2RlLnBvc2l0aW9uCiAgICAgICAgICAgICAgICBwb3MgPSAocFswXSt4Ym9tYixwWzFdKzUscFsyXS14Ym9tYikKICAgICAgICAgICAgICAgIGJhbGwgPSBib21iLkJvbWIocG9zaXRpb249cG9zLGJvbWJfdHlwZT0naW1wYWN0JykuYXV0b3JldGFpbigpCiAgICAgICAgICAgICAgICBiYWxsLm5vZGUubW9kZWxfc2NhbGUgPSAwLjYKICAgICAgICAgICAgICAgIGJhbGwubm9kZS5tb2RlbCA9IGJhLmdldG1vZGVsKCdlZ2cnKQogICAgICAgICAgICAgICAgYmFsbC5ub2RlLmdyYXZpdHlfc2NhbGUgPSAyCgogICAgICAgIGlmIHNlbGYuZWRnX2VmZjoKICAgICAgICAgICAgc2VsZi5lZGdfZWZmID0gRmFsc2UKCiAgICAgICAgd2FzZGVhZCA9IHNlbGYuX2RlYWQKICAgICAgICBzZWxmLl9kZWFkID0gVHJ1ZQogICAgICAgIHNlbGYuaGl0cG9pbnRzID0gMAogICAgICAgIGlmIG1zZy5pbW1lZGlhdGU6CiAgICAgICAgICAgIGlmIHNlbGYubm9kZToKICAgICAgICAgICAgICAgIHNlbGYubm9kZS5kZWxldGUoKQogICAgICAgIGVsaWYgc2VsZi5ub2RlOgogICAgICAgICAgICBzZWxmLm5vZGUuaHVydCA9IDEuMAogICAgICAgICAgICBpZiBzZWxmLnBsYXlfYmlnX2RlYXRoX3NvdW5kIGFuZCBub3Qgd2FzZGVhZDoKICAgICAgICAgICAgICAgIGJhLnBsYXlzb3VuZChTcGF6RmFjdG9yeS5nZXQoKS5zaW5nbGVfcGxheWVyX2RlYXRoX3NvdW5kKQogICAgICAgICAgICBzZWxmLm5vZGUuZGVhZCA9IFRydWUKICAgICAgICAgICAgYmEudGltZXIoMi4wLCBzZWxmLm5vZGUuZGVsZXRlKQoKICAgICAgICAgICAgdCA9IDAKICAgICAgICAgICAgaWYgc2VsZi5raWxsX2VmZjoKICAgICAgICAgICAgICAgIGZvciBib21icyBpbiByYW5nZSgzKToKICAgICAgICAgICAgICAgICAgICBiYS50aW1lcih0LGJhLkNhbGwoZHJvcF9ib21iKSkKICAgICAgICAgICAgICAgICAgICB0ICs9IDAuMTUKICAgICAgICAgICAgICAgIHNlbGYua2lsbF9lZmYgPSBGYWxzZQoKICAgIGVsaWYgaXNpbnN0YW5jZShtc2csIGJhLk91dE9mQm91bmRzTWVzc2FnZSk6CiAgICAgICAgc2VsZi5oYW5kbGVtZXNzYWdlKGJhLkRpZU1lc3NhZ2UoaG93PWJhLkRlYXRoVHlwZS5GQUxMKSkKCiAgICBlbGlmIGlzaW5zdGFuY2UobXNnLCBiYS5TdGFuZE1lc3NhZ2UpOgogICAgICAgIHNlbGYuX2xhc3Rfc3RhbmRfcG9zID0gKG1zZy5wb3NpdGlvblswXSwgbXNnLnBvc2l0aW9uWzFdLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1zZy5wb3NpdGlvblsyXSkKICAgICAgICBpZiBzZWxmLm5vZGU6CiAgICAgICAgICAgIHNlbGYubm9kZS5oYW5kbGVtZXNzYWdlKCdzdGFuZCcsIG1zZy5wb3NpdGlvblswXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbXNnLnBvc2l0aW9uWzFdLCBtc2cucG9zaXRpb25bMl0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1zZy5hbmdsZSkKCiAgICBlbGlmIGlzaW5zdGFuY2UobXNnLCBDdXJzZUV4cGxvZGVNZXNzYWdlKToKICAgICAgICBzZWxmLmN1cnNlX2V4cGxvZGUoKQoKICAgIGVsaWYgaXNpbnN0YW5jZShtc2csIFB1bmNoSGl0TWVzc2FnZSk6CiAgICAgICAgaWYgbm90IHNlbGYubm9kZToKICAgICAgICAgICAgcmV0dXJuIE5vbmUKICAgICAgICBub2RlID0gYmEuZ2V0Y29sbGlzaW9uKCkub3Bwb3Npbmdub2RlCgogICAgICAgIGlmIG5vZGUgYW5kIChub2RlIG5vdCBpbiBzZWxmLl9wdW5jaGVkX25vZGVzKToKCiAgICAgICAgICAgIHB1bmNoX21vbWVudHVtX2FuZ3VsYXIgPSAoc2VsZi5ub2RlLnB1bmNoX21vbWVudHVtX2FuZ3VsYXIgKgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNlbGYuX3B1bmNoX3Bvd2VyX3NjYWxlKQogICAgICAgICAgICBwdW5jaF9wb3dlciA9IHNlbGYubm9kZS5wdW5jaF9wb3dlciAqIHNlbGYuX3B1bmNoX3Bvd2VyX3NjYWxlCgogICAgICAgICAgICBpZiBub2RlLmdldG5vZGV0eXBlKCkgIT0gJ3NwYXonOgogICAgICAgICAgICAgICAgc291bmRzID0gU3BhekZhY3RvcnkuZ2V0KCkuaW1wYWN0X3NvdW5kc19tZWRpdW0KICAgICAgICAgICAgICAgIHNvdW5kID0gc291bmRzW3JhbmRvbS5yYW5kcmFuZ2UobGVuKHNvdW5kcykpXQogICAgICAgICAgICAgICAgYmEucGxheXNvdW5kKHNvdW5kLCAxLjAsIHBvc2l0aW9uPXNlbGYubm9kZS5wb3NpdGlvbikKCiAgICAgICAgICAgIHBwb3MgPSBzZWxmLm5vZGUucHVuY2hfcG9zaXRpb24KICAgICAgICAgICAgcHVuY2hkaXIgPSBzZWxmLm5vZGUucHVuY2hfdmVsb2NpdHkKICAgICAgICAgICAgdmVsID0gc2VsZi5ub2RlLnB1bmNoX21vbWVudHVtX2xpbmVhcgoKICAgICAgICAgICAgc2VsZi5fcHVuY2hlZF9ub2Rlcy5hZGQobm9kZSkKICAgICAgICAgICAgbm9kZS5oYW5kbGVtZXNzYWdlKAogICAgICAgICAgICAgICAgYmEuSGl0TWVzc2FnZSgKICAgICAgICAgICAgICAgICAgICBwb3M9cHBvcywKICAgICAgICAgICAgICAgICAgICB2ZWxvY2l0eT12ZWwsCiAgICAgICAgICAgICAgICAgICAgbWFnbml0dWRlPXB1bmNoX3Bvd2VyICogcHVuY2hfbW9tZW50dW1fYW5ndWxhciAqIDExMC4wLAogICAgICAgICAgICAgICAgICAgIHZlbG9jaXR5X21hZ25pdHVkZT1wdW5jaF9wb3dlciAqIDQwLAogICAgICAgICAgICAgICAgICAgIHJhZGl1cz0wLAogICAgICAgICAgICAgICAgICAgIHNyY25vZGU9c2VsZi5ub2RlLAogICAgICAgICAgICAgICAgICAgIHNvdXJjZV9wbGF5ZXI9c2VsZi5zb3VyY2VfcGxheWVyLAogICAgICAgICAgICAgICAgICAgIGZvcmNlX2RpcmVjdGlvbj1wdW5jaGRpciwKICAgICAgICAgICAgICAgICAgICBoaXRfdHlwZT0ncHVuY2gnLAogICAgICAgICAgICAgICAgICAgIGhpdF9zdWJ0eXBlPSgnc3VwZXJfcHVuY2gnIGlmIHNlbGYuX2hhc19ib3hpbmdfZ2xvdmVzCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGVsc2UgJ2RlZmF1bHQnKSkpCgogICAgICAgICAgICBtYWcgPSAtNDAwLjAKICAgICAgICAgICAgaWYgc2VsZi5faG9ja2V5OgogICAgICAgICAgICAgICAgbWFnICo9IDAuNQogICAgICAgICAgICBpZiBsZW4oc2VsZi5fcHVuY2hlZF9ub2RlcykgPT0gMToKICAgICAgICAgICAgICAgIHNlbGYubm9kZS5oYW5kbGVtZXNzYWdlKCdraWNrX2JhY2snLCBwcG9zWzBdLCBwcG9zWzFdLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcHBvc1syXSwgcHVuY2hkaXJbMF0sIHB1bmNoZGlyWzFdLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcHVuY2hkaXJbMl0sIG1hZykKICAgIGVsaWYgaXNpbnN0YW5jZShtc2csIFBpY2t1cE1lc3NhZ2UpOgogICAgICAgIGlmIG5vdCBzZWxmLm5vZGU6CiAgICAgICAgICAgIHJldHVybiBOb25lCgogICAgICAgIHRyeToKICAgICAgICAgICAgY29sbGlzaW9uID0gYmEuZ2V0Y29sbGlzaW9uKCkKICAgICAgICAgICAgb3Bwb3Npbmdub2RlID0gY29sbGlzaW9uLm9wcG9zaW5nbm9kZQogICAgICAgICAgICBvcHBvc2luZ2JvZHkgPSBjb2xsaXNpb24ub3Bwb3Npbmdib2R5CiAgICAgICAgZXhjZXB0IGJhLk5vdEZvdW5kRXJyb3I6CiAgICAgICAgICAgIHJldHVybiBUcnVlCgogICAgICAgIHRyeToKICAgICAgICAgICAgaWYgb3Bwb3Npbmdub2RlLmludmluY2libGU6CiAgICAgICAgICAgICAgICByZXR1cm4gVHJ1ZQogICAgICAgIGV4Y2VwdCBFeGNlcHRpb246CiAgICAgICAgICAgIHBhc3MKCiAgICAgICAgaWYgKG9wcG9zaW5nbm9kZS5nZXRub2RldHlwZSgpID09ICdzcGF6JwogICAgICAgICAgICAgICAgYW5kIG5vdCBvcHBvc2luZ25vZGUuc2hhdHRlcmVkIGFuZCBvcHBvc2luZ2JvZHkgPT0gNCk6CiAgICAgICAgICAgIG9wcG9zaW5nYm9keSA9IDEKCiAgICAgICAgaGVsZCA9IHNlbGYubm9kZS5ob2xkX25vZGUKICAgICAgICBpZiBoZWxkIGFuZCBoZWxkLmdldG5vZGV0eXBlKCkgPT0gJ2ZsYWcnOgogICAgICAgICAgICByZXR1cm4gVHJ1ZQoKICAgICAgICBzZWxmLm5vZGUuaG9sZF9ib2R5ID0gb3Bwb3Npbmdib2R5CiAgICAgICAgc2VsZi5ub2RlLmhvbGRfbm9kZSA9IG9wcG9zaW5nbm9kZQogICAgZWxpZiBpc2luc3RhbmNlKG1zZywgYmEuQ2VsZWJyYXRlTWVzc2FnZSk6CiAgICAgICAgaWYgc2VsZi5ub2RlOgogICAgICAgICAgICBzZWxmLm5vZGUuaGFuZGxlbWVzc2FnZSgnY2VsZWJyYXRlJywgaW50KG1zZy5kdXJhdGlvbiAqIDEwMDApKQoKICAgIHJldHVybiBOb25lCiAgICAgICAgCmNsYXNzIFBvd2VydXBNYW5hZ2VyV2luZG93KFBvcHVwV2luZG93KToKICAgIGRlZiBfX2luaXRfXyhzZWxmLCB0cmFuc2l0aW9uPSAnaW5fcmlnaHQnKToKICAgICAgICBjb2x1bW5zID0gMgogICAgICAgIHNlbGYuX3dpZHRoID0gd2lkdGggPSA4MDAKICAgICAgICBzZWxmLl9oZWlnaHQgPSBoZWlnaHQgPSA1MDAKICAgICAgICBzZWxmLl9zdWJfaGVpZ2h0ID0gMjAwCiAgICAgICAgc2VsZi5fc2Nyb2xsX3dpZHRoID0gc2VsZi5fd2lkdGgqMC45MAogICAgICAgIHNlbGYuX3Njcm9sbF9oZWlnaHQgPSBzZWxmLl9oZWlnaHQgLSAxODAKICAgICAgICBzZWxmLl9zdWJfd2lkdGggPSBzZWxmLl9zY3JvbGxfd2lkdGgqMC45NTsKICAgICAgICBzZWxmLnRhYl9idXR0b25zOiBzZXQgPSB7fQogICAgICAgIHNlbGYubGlzdF9jbHNfcG93ZXI6IGxpc3QgPSBbXQogICAgICAgIHNlbGYuZGVmYXVsdF9wb3dlcnVwcyA9IGRlZmF1bHRfcG93ZXJ1cHMoKQogICAgICAgIHNlbGYuZGVmYXVsdF9wb3dlcl9saXN0ID0gbGlzdChzZWxmLmRlZmF1bHRfcG93ZXJ1cHMpCiAgICAgICAgc2VsZi5jb2lucyA9IGFwZ1snQmVhciBDb2luJ10KICAgICAgICBzZWxmLnBvcHVwX2Nsc19wb3dlciA9IE5vbmUKCiAgICAgICAgaWYgbm90IFNUT1JFWydCdXkgRmlyZWJvbWJzJ106CiAgICAgICAgICAgIHBvd2VydXBzWydGaXJlIEJvbWJzJ10gPSAwCiAgICAgICAgICAgIHNlbGYuZGVmYXVsdF9wb3dlcl9saXN0LnJlbW92ZSgnRmlyZSBCb21icycpCgogICAgICAgIHNlbGYuY2hhcnN0ciA9IFtiYS5jaGFyc3RyKGJhLlNwZWNpYWxDaGFyLkxFRlRfQVJST1cpLAogICAgICAgICAgICAgICAgICAgICAgICBiYS5jaGFyc3RyKGJhLlNwZWNpYWxDaGFyLlJJR0hUX0FSUk9XKSwKICAgICAgICAgICAgICAgICAgICAgICAgYmEuY2hhcnN0cihiYS5TcGVjaWFsQ2hhci5VUF9BUlJPVyksCiAgICAgICAgICAgICAgICAgICAgICAgIGJhLmNoYXJzdHIoYmEuU3BlY2lhbENoYXIuRE9XTl9BUlJPVyldCgogICAgICAgIHNlbGYudGFiZGVmcyA9IHsiQWN0aW9uIDEiOiBbJ3Bvd2VydXBJY2VCb21icycsKDEsMSwxKV0sCiAgICAgICAgICAgICAgICAgICAgICAgICJBY3Rpb24gMiI6IFsnc2V0dGluZ3NJY29uJywoMCwxLDApXSwKICAgICAgICAgICAgICAgICAgICAgICAgIkFjdGlvbiAzIjogWydpbnZlbnRvcnlJY29uJywoMSwxLDEpXSwKICAgICAgICAgICAgICAgICAgICAgICAgIkFjdGlvbiA0IjogWydzdG9yZUljb24nLCgxLDEsMSldLAogICAgICAgICAgICAgICAgICAgICAgICAiQWN0aW9uIDUiOiBbJ2FkdmFuY2VkSWNvbicsKDEsMSwxKV0sCiAgICAgICAgICAgICAgICAgICAgICAgICJBYm91dCI6IFsnaGVhcnQnLCgxLjUsMC4zLDAuMyldfQogICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICBpZiAoU1RPUkVbJ0J1eSBGaXJlYm9tYnMnXSBhbmQKICAgICAgICAgICAgU1RPUkVbJ0J1eSBPcHRpb24nXSBhbmQKICAgICAgICAgICAgU1RPUkVbJ0J1eSBQZXJjZW50YWdlJ10pOgogICAgICAgICAgICBzZWxmLnRhYmRlZnMgPSB7IkFjdGlvbiAxIjogWydwb3dlcnVwSWNlQm9tYnMnLCgxLDEsMSldLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIkFjdGlvbiAyIjogWydzZXR0aW5nc0ljb24nLCgwLDEsMCldLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIkFjdGlvbiAzIjogWydpbnZlbnRvcnlJY29uJywoMSwxLDEpXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJBYm91dCI6IFsnaGVhcnQnLCgxLjUsMC4zLDAuMyldfQogICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICBzZWxmLmxpc3RkZWYgPSBsaXN0KHNlbGYudGFiZGVmcykKICAgICAgICAKICAgICAgICBzZWxmLmNvdW50ID0gbGVuKHNlbGYudGFiZGVmcykKICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgc2VsZi5fY3VycmVudF90YWIgPSBHTE9CQUxbJ1RhYiddCgogICAgICAgIGFwcCA9IGJhLmFwcC51aQogICAgICAgIHVpc2NhbGUgPSBhcHAudWlzY2FsZQoKICAgICAgICBzZWxmLl9yb290X3dpZGdldCA9IGJhLmNvbnRhaW5lcndpZGdldChzaXplPSh3aWR0aCs5MCxoZWlnaHQrODApLHRyYW5zaXRpb249dHJhbnNpdGlvbiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgc2NhbGU9MS41IGlmIHVpc2NhbGUgaXMgYmEuVUlTY2FsZS5TTUFMTCBlbHNlIDEuMCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RhY2tfb2Zmc2V0PSgwLC0zMCkgaWYgdWlzY2FsZSBpcyBiYS5VSVNjYWxlLlNNQUxMIGVsc2UgICgwLDApKQogICAgICAgIAogICAgICAgIHNlbGYuX2JhY2tCdXR0b24gPSBiID0gYmEuYnV0dG9ud2lkZ2V0KHBhcmVudD1zZWxmLl9yb290X3dpZGdldCxhdXRvc2VsZWN0PVRydWUsCiAgICAgICAgICAgICAgICAgICAgICAgcG9zaXRpb249KDYwLHNlbGYuX2hlaWdodC0xNSksc2l6ZT0oMTMwLDYwKSwKICAgICAgICAgICAgICAgICAgICAgICBzY2FsZT0wLjgsdGV4dF9zY2FsZT0xLjIsbGFiZWw9YmEuTHN0cihyZXNvdXJjZT0nYmFja1RleHQnKSwKICAgICAgICAgICAgICAgICAgICAgICBidXR0b25fdHlwZT0nYmFjaycsb25fYWN0aXZhdGVfY2FsbD1iYS5DYWxsKHNlbGYuX2JhY2spKQogICAgICAgIGJhLmJ1dHRvbndpZGdldChlZGl0PXNlbGYuX2JhY2tCdXR0b24sIGJ1dHRvbl90eXBlPSdiYWNrU21hbGwnLHNpemU9KDYwLCA2MCksbGFiZWw9YmEuY2hhcnN0cihiYS5TcGVjaWFsQ2hhci5CQUNLKSkKICAgICAgICBiYS5jb250YWluZXJ3aWRnZXQoZWRpdD1zZWxmLl9yb290X3dpZGdldCxjYW5jZWxfYnV0dG9uPWIpCgogICAgICAgIHNlbGYudGl0bGV0ZXh0ID0gYmEudGV4dHdpZGdldChwYXJlbnQ9c2VsZi5fcm9vdF93aWRnZXQscG9zaXRpb249KDAsIGhlaWdodC0xNSksc2l6ZT0od2lkdGgsNTApLAogICAgICAgICAgICAgICAgICAgICAgICAgIGhfYWxpZ249ImNlbnRlciIsY29sb3I9YmEuYXBwLnVpLnRpdGxlX2NvbG9yLCB2X2FsaWduPSJjZW50ZXIiLG1heHdpZHRoPXdpZHRoKjEuMykKICAgICAgICAKICAgICAgICBpbmRleCA9IDAKICAgICAgICBmb3IgdGFiIGluIHJhbmdlKHNlbGYuY291bnQpOgogICAgICAgICAgICBmb3IgdGFiMiBpbiByYW5nZShjb2x1bW5zKToKICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgdGFnID0gc2VsZi5saXN0ZGVmW2luZGV4XQogICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICBwb3NpdGlvbiA9ICg2MjArKHRhYjIqMTIwKSxzZWxmLl9oZWlnaHQtNTAqMi41LSh0YWIqMTIwKSkKICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgaWYgdGFnID09ICdBYm91dCc6CiAgICAgICAgICAgICAgICAgICAgdGV4dCA9IGJhLkxzdHIocmVzb3VyY2U9J2dhdGhlcldpbmRvdy5hYm91dFRleHQnKQogICAgICAgICAgICAgICAgZWxpZiB0YWIgPT0gJ0FjdGlvbiA0JzoKICAgICAgICAgICAgICAgICAgICB0ZXh0ID0gYmEuTHN0cihyZXNvdXJjZT0nc3RvcmVUZXh0JykKICAgICAgICAgICAgICAgIGVsc2U6IHRleHQgPSBnZXRsYW5ndWFnZSh0YWcpCiAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgIHNlbGYudGFiX2J1dHRvbnNbdGFnXSA9IGJhLmJ1dHRvbndpZGdldChwYXJlbnQ9c2VsZi5fcm9vdF93aWRnZXQsYXV0b3NlbGVjdD1UcnVlLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcG9zaXRpb249cG9zaXRpb24sc2l6ZT0oMTEwLDExMCksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzY2FsZT0xLGxhYmVsPScnLGVuYWJsZV9zb3VuZD1GYWxzZSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJ1dHRvbl90eXBlPSdzcXVhcmUnLG9uX2FjdGl2YXRlX2NhbGw9YmEuQ2FsbChzZWxmLl9zZXRfdGFiLHRhZyxzb3VuZD1UcnVlKSkKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICBzZWxmLnRleHQgPSBiYS50ZXh0d2lkZ2V0KHBhcmVudD1zZWxmLl9yb290X3dpZGdldCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBvc2l0aW9uPShwb3NpdGlvblswXSs1NSxwb3NpdGlvblsxXSszMCksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBzaXplPSgwLCAwKSxzY2FsZT0xLGNvbG9yPWJhLmFwcC51aS50aXRsZV9jb2xvciwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIGRyYXdfY29udHJvbGxlcj1zZWxmLnRhYl9idXR0b25zW3RhZ10sbWF4d2lkdGg9MTAwLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgdGV4dD10ZXh0LGhfYWxpZ249J2NlbnRlcicsdl9hbGlnbj0nY2VudGVyJykKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICBzZWxmLmltYWdlID0gYmEuaW1hZ2V3aWRnZXQocGFyZW50PXNlbGYuX3Jvb3Rfd2lkZ2V0LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNpemU9KDYwLDYwKSxjb2xvcj1zZWxmLnRhYmRlZnNbdGFnXVsxXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkcmF3X2NvbnRyb2xsZXI9c2VsZi50YWJfYnV0dG9uc1t0YWddLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBvc2l0aW9uPShwb3NpdGlvblswXSsyNSxwb3NpdGlvblsxXSs0MCksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdGV4dHVyZT1iYS5nZXR0ZXh0dXJlKHNlbGYudGFiZGVmc1t0YWddWzBdKSkKCiAgICAgICAgICAgICAgICBpbmRleCArPSAxCiAgICAgICAgCiAgICAgICAgICAgICAgICBpZiBzZWxmLmNvdW50ID09IGluZGV4OgogICAgICAgICAgICAgICAgICAgIGJyZWFrCiAgICAgICAKICAgICAgICAgICAgaWYgc2VsZi5jb3VudCA9PSBpbmRleDoKICAgICAgICAgICAgICAgIGJyZWFrCiAgICAgICAgCiAgICAgICAgc2VsZi5fc2Nyb2xsd2lkZ2V0ID0gTm9uZQogICAgICAgIHNlbGYuX3RhYl9jb250YWluZXIgPSBOb25lCiAgICAgICAgc2VsZi5fc2V0X3RhYihzZWxmLl9jdXJyZW50X3RhYikKCiAgICBkZWYgX19kZWxfXyhzZWxmKToKICAgICAgICBhcGcuYXBwbHlfYW5kX2NvbW1pdCgpCgogICAgZGVmIF9zZXRfdGFiKHNlbGYsIHRhYiwgc291bmQ6IGJvb2wgPSBGYWxzZSk6CiAgICAgICAgc2VsZi5zb3VuZCA9IHNvdW5kCiAgICAgICAgR0xPQkFMWydUYWInXSA9IHRhYgogICAgICAgIGFwZy5hcHBseV9hbmRfY29tbWl0KCkKICAgICAgICAKICAgICAgICBpZiBzZWxmLl90YWJfY29udGFpbmVyIGlzIG5vdCBOb25lIGFuZCBzZWxmLl90YWJfY29udGFpbmVyLmV4aXN0cygpOgogICAgICAgICAgICBzZWxmLl90YWJfY29udGFpbmVyLmRlbGV0ZSgpCgogICAgICAgIGlmIHNlbGYuc291bmQ6CiAgICAgICAgICAgIGJhLnBsYXlzb3VuZChiYS5nZXRzb3VuZCgnY2xpY2swMScpKQoKICAgICAgICBpZiBzZWxmLl9zY3JvbGx3aWRnZXQ6CiAgICAgICAgICAgIHNlbGYuX3Njcm9sbHdpZGdldC5kZWxldGUoKQoKICAgICAgICBzZWxmLl9zY3JvbGx3aWRnZXQgPSBiYS5zY3JvbGx3aWRnZXQocGFyZW50PXNlbGYuX3Jvb3Rfd2lkZ2V0LAogICAgICAgICAgICBwb3NpdGlvbj0oc2VsZi5fd2lkdGgqMC4wOCw1MSoxLjgpLHNpemU9KHNlbGYuX3N1Yl93aWR0aCAtMTQwLHNlbGYuX3Njcm9sbF9oZWlnaHQgKzYwKjEuMikpCgogICAgICAgIGlmIHRhYiA9PSAnQWN0aW9uIDQnOgogICAgICAgICAgICBpZiBzZWxmLl9zY3JvbGx3aWRnZXQ6CiAgICAgICAgICAgICAgICBzZWxmLl9zY3JvbGx3aWRnZXQuZGVsZXRlKCkKICAgICAgICAgICAgc2VsZi5fc2Nyb2xsd2lkZ2V0ID0gYmEuaHNjcm9sbHdpZGdldChwYXJlbnQ9c2VsZi5fcm9vdF93aWRnZXQsCiAgICAgICAgICAgICAgICBwb3NpdGlvbj0oc2VsZi5fd2lkdGgqMC4wOCw1MSoxLjgpLHNpemU9KHNlbGYuX3N1Yl93aWR0aCAtMTQwLHNlbGYuX3Njcm9sbF9oZWlnaHQgKzYwKjEuMiksCiAgICAgICAgICAgICAgICBjYXB0dXJlX2Fycm93cz1UcnVlLGNsYWltc19sZWZ0X3JpZ2h0PVRydWUpCiAgICAgICAgICAgIGJhLnRleHR3aWRnZXQoZWRpdD1zZWxmLnRpdGxldGV4dCx0ZXh0PWJhLkxzdHIocmVzb3VyY2U9J3N0b3JlVGV4dCcpKQogICAgICAgIGVsaWYgdGFiID09ICdBYm91dCc6CiAgICAgICAgICAgIGJhLnRleHR3aWRnZXQoZWRpdD1zZWxmLnRpdGxldGV4dCx0ZXh0PWJhLkxzdHIocmVzb3VyY2U9J2dhdGhlcldpbmRvdy5hYm91dFRleHQnKSkKICAgICAgICBlbHNlOiBiYS50ZXh0d2lkZ2V0KGVkaXQ9c2VsZi50aXRsZXRleHQsdGV4dD1nZXRsYW5ndWFnZSh0YWIpKQoKICAgICAgICBjaG9pY2VzID0gWydSZXNldCcsJ09ubHkgQm9tYnMnLCdPbmx5IEl0ZW1zJywnTmV3JywnTm90aGluZyddCiAgICAgICAgY19kaXNwbGF5ID0gW10KICAgICAgICAKICAgICAgICBmb3IgZGlzcGxheSBpbiBjaG9pY2VzOgogICAgICAgICAgICBjaG9pY2VzX2Rpc3BsYXkgPSBiYS5Mc3RyKHRyYW5zbGF0ZT0oIiIsZ2V0bGFuZ3VhZ2UoZGlzcGxheSkpKQogICAgICAgICAgICBjX2Rpc3BsYXkuYXBwZW5kKGNob2ljZXNfZGlzcGxheSkKICAgIAogICAgICAgIGlmIHRhYiA9PSAnQWN0aW9uIDEnOgogICAgICAgICAgICBzZWxmLnBvcHVwX2Nsc19wb3dlciA9IFBvcHVwTWVudSgKICAgICAgICAgICAgICAgICAgcGFyZW50PXNlbGYuX3Jvb3Rfd2lkZ2V0LAogICAgICAgICAgICAgICAgICBwb3NpdGlvbj0oMTMwLHNlbGYuX3dpZHRoKjAuNjEpLAogICAgICAgICAgICAgICAgICBidXR0b25fc2l6ZT0oMTUwLDUwKSxzY2FsZT0yLjUsCiAgICAgICAgICAgICAgICAgIGNob2ljZXM9Y2hvaWNlcyx3aWR0aD0xNTAsCiAgICAgICAgICAgICAgICAgIGNob2ljZXNfZGlzcGxheT1jX2Rpc3BsYXksCiAgICAgICAgICAgICAgICAgIGN1cnJlbnRfY2hvaWNlPUdMT0JBTFsnQ2xzIFBvd2VydXAnXSwKICAgICAgICAgICAgICAgICAgb25fdmFsdWVfY2hhbmdlX2NhbGw9c2VsZi5fc2V0X2NvbmNlcHQpCiAgICAgICAgICAgIHNlbGYubGlzdF9jbHNfcG93ZXIuYXBwZW5kKHNlbGYucG9wdXBfY2xzX3Bvd2VyLl9idXR0b24pCiAgICAgICAgICAgIAogICAgICAgICAgICBzZWxmLmJ1dHRvbl9jbHNfcG93ZXIgPSBiYS5idXR0b253aWRnZXQocGFyZW50PXNlbGYuX3Jvb3Rfd2lkZ2V0LAogICAgICAgICAgICAgICAgICAgIHBvc2l0aW9uPSg1MDAsc2VsZi5fd2lkdGgqMC42MSksc2l6ZT0oNTAsNTApLGF1dG9zZWxlY3Q9VHJ1ZSwKICAgICAgICAgICAgICAgICAgICBzY2FsZT0xLGxhYmVsPSgnJScpLHRleHRfc2NhbGU9MSxidXR0b25fdHlwZT0nc3F1YXJlJywKICAgICAgICAgICAgICAgICAgICBvbl9hY3RpdmF0ZV9jYWxsPXNlbGYuX3BlcmNlbnRhZ2Vfd2luZG93KSAKICAgICAgICAgICAgc2VsZi5saXN0X2Nsc19wb3dlci5hcHBlbmQoc2VsZi5idXR0b25fY2xzX3Bvd2VyKQogICAgICAgICAgICAKICAgICAgICAgICAgcmV3aW5kb3cgPSBbc2VsZi5wb3B1cF9jbHNfcG93ZXIuX2J1dHRvbixzZWxmLmJ1dHRvbl9jbHNfcG93ZXJdCiAgICAgICAgICAgIAogICAgICAgICAgICBmb3IgY2xzIGluIHNlbGYubGlzdF9jbHNfcG93ZXI6ICMgdGhpcyBpcyB2ZXJ5IGltcG9ydGFudCBzbyB0aGF0IHB1cHVwcyBkb24ndCBhY2N1bXVsYXRlCiAgICAgICAgICAgICAgICBpZiBjbHMgbm90IGluIHJld2luZG93OgogICAgICAgICAgICAgICAgICAgIGNscy5kZWxldGUoKQogICAgICAgICAgICAKICAgICAgICBlbGlmIHRhYiA9PSAnQWN0aW9uIDQnOgogICAgICAgICAgICBzZWxmLmJ1dHRvbl9jb2luID0gYmEuYnV0dG9ud2lkZ2V0KHBhcmVudD1zZWxmLl9yb290X3dpZGdldCxpY29uPWJhLmdldHRleHR1cmUoJ2NvaW4nKSwKICAgICAgICAgICAgICAgICAgICBwb3NpdGlvbj0oNTUwLHNlbGYuX3dpZHRoKjAuNjE0KSxzaXplPSgxNjAsNDApLHRleHRjb2xvcj0oMCwxLDApLGNvbG9yPSgwLDEsNiksCiAgICAgICAgICAgICAgICAgICAgc2NhbGU9MSxsYWJlbD1zdHIoYXBnWydCZWFyIENvaW4nXSksdGV4dF9zY2FsZT0xLGF1dG9zZWxlY3Q9VHJ1ZSwKICAgICAgICAgICAgICAgICAgICBvbl9hY3RpdmF0ZV9jYWxsPU5vbmUpICNzZWxmLl9wZXJjZW50YWdlX3dpbmRvdykKICAgICAgICAgICAgc2VsZi5saXN0X2Nsc19wb3dlci5hcHBlbmQoc2VsZi5idXR0b25fY29pbikKICAgICAgICAgICAgCiAgICAgICAgICAgIHRyeTogcmV3aW5kb3cuYXBwZW5kKHNlbGYuYnV0dG9uX2NvaW4pCiAgICAgICAgICAgIGV4Y2VwdDogcmV3aW5kb3cgPSBbc2VsZi5idXR0b25fY29pbl0KICAgICAgICAgICAgZm9yIGNscyBpbiBzZWxmLmxpc3RfY2xzX3Bvd2VyOiAjIHRoaXMgaXMgdmVyeSBpbXBvcnRhbnQgc28gdGhhdCBwdXB1cHMgZG9uJ3QgYWNjdW11bGF0ZQogICAgICAgICAgICAgICAgaWYgY2xzIG5vdCBpbiByZXdpbmRvdzoKICAgICAgICAgICAgICAgICAgICBjbHMuZGVsZXRlKCkKICAgICAgICAgICAgCiAgICAgICAgZWxzZToKICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgZm9yIGNscyBpbiBzZWxmLmxpc3RfY2xzX3Bvd2VyOgogICAgICAgICAgICAgICAgICAgIGNscy5kZWxldGUoKQogICAgICAgICAgICBleGNlcHQ6IHBhc3MKCiAgICAgICAgaWYgdGFiID09ICdBY3Rpb24gMSc6CiAgICAgICAgICAgIHN1Yl9oZWlnaHQgPSBsZW4oc2VsZi5kZWZhdWx0X3Bvd2VyX2xpc3QpICogOTAKICAgICAgICAgICAgdiA9IHN1Yl9oZWlnaHQgLSA1NQogICAgICAgICAgICB3aWR0aCA9IDMwMAogICAgICAgICAgICBwb3NpID0gMAogICAgICAgICAgICBpZF9wb3dlciA9IGxpc3Qoc2VsZi5kZWZhdWx0X3Bvd2VydXBzKQogICAgICAgICAgICBuZXdfcG93ZXJ1cHMgPSBpZF9wb3dlcls5Ol0KICAgICAgICAgICAgc2VsZi5saXN0cG93ZXIgPSB7fQogICAgICAgICAgICAKICAgICAgICAgICAgc2VsZi5fdGFiX2NvbnRhaW5lciA9IGMgPSBiYS5jb250YWluZXJ3aWRnZXQocGFyZW50PXNlbGYuX3Njcm9sbHdpZGdldCwKICAgICAgICAgICAgICAgIHNpemU9KHNlbGYuX3N1Yl93aWR0aCxzdWJfaGVpZ2h0KSwKICAgICAgICAgICAgICAgIGJhY2tncm91bmQ9RmFsc2Usc2VsZWN0aW9uX2xvb3BzX3RvX3BhcmVudD1UcnVlKQoKICAgICAgICAgICAgZm9yIHBvd2VyIGluIHNlbGYuZGVmYXVsdF9wb3dlcl9saXN0OgogICAgICAgICAgICAgICAgaWYgcG93ZXIgPT0gaWRfcG93ZXJbMF06CiAgICAgICAgICAgICAgICAgICAgdGV4dCA9ICdoZWxwV2luZG93LnBvd2VydXBTaGllbGROYW1lVGV4dCcKICAgICAgICAgICAgICAgICAgICB0ZXggPSBiYS5nZXR0ZXh0dXJlKCdwb3dlcnVwU2hpZWxkJykKICAgICAgICAgICAgICAgIGVsaWYgcG93ZXIgPT0gaWRfcG93ZXJbMV06CiAgICAgICAgICAgICAgICAgICAgdGV4dCA9ICdoZWxwV2luZG93LnBvd2VydXBQdW5jaE5hbWVUZXh0JwogICAgICAgICAgICAgICAgICAgIHRleCA9IGJhLmdldHRleHR1cmUoJ3Bvd2VydXBQdW5jaCcpCiAgICAgICAgICAgICAgICBlbGlmIHBvd2VyID09IGlkX3Bvd2VyWzJdOgogICAgICAgICAgICAgICAgICAgIHRleHQgPSAnaGVscFdpbmRvdy5wb3dlcnVwTGFuZE1pbmVzTmFtZVRleHQnCiAgICAgICAgICAgICAgICAgICAgdGV4ID0gYmEuZ2V0dGV4dHVyZSgncG93ZXJ1cExhbmRNaW5lcycpCiAgICAgICAgICAgICAgICBlbGlmIHBvd2VyID09IGlkX3Bvd2VyWzNdOgogICAgICAgICAgICAgICAgICAgIHRleHQgPSAnaGVscFdpbmRvdy5wb3dlcnVwSW1wYWN0Qm9tYnNOYW1lVGV4dCcKICAgICAgICAgICAgICAgICAgICB0ZXggPSBiYS5nZXR0ZXh0dXJlKCdwb3dlcnVwSW1wYWN0Qm9tYnMnKQogICAgICAgICAgICAgICAgZWxpZiBwb3dlciA9PSBpZF9wb3dlcls0XToKICAgICAgICAgICAgICAgICAgICB0ZXh0ID0gJ2hlbHBXaW5kb3cucG93ZXJ1cEljZUJvbWJzTmFtZVRleHQnCiAgICAgICAgICAgICAgICAgICAgdGV4ID0gYmEuZ2V0dGV4dHVyZSgncG93ZXJ1cEljZUJvbWJzJykKICAgICAgICAgICAgICAgIGVsaWYgcG93ZXIgPT0gaWRfcG93ZXJbNV06CiAgICAgICAgICAgICAgICAgICAgdGV4dCA9ICdoZWxwV2luZG93LnBvd2VydXBCb21iTmFtZVRleHQnCiAgICAgICAgICAgICAgICAgICAgdGV4ID0gYmEuZ2V0dGV4dHVyZSgncG93ZXJ1cEJvbWInKQogICAgICAgICAgICAgICAgZWxpZiBwb3dlciA9PSBpZF9wb3dlcls2XToKICAgICAgICAgICAgICAgICAgICB0ZXh0ID0gJ2hlbHBXaW5kb3cucG93ZXJ1cFN0aWNreUJvbWJzTmFtZVRleHQnCiAgICAgICAgICAgICAgICAgICAgdGV4ID0gYmEuZ2V0dGV4dHVyZSgncG93ZXJ1cFN0aWNreUJvbWJzJykKICAgICAgICAgICAgICAgIGVsaWYgcG93ZXIgPT0gaWRfcG93ZXJbN106CiAgICAgICAgICAgICAgICAgICAgdGV4dCA9ICdoZWxwV2luZG93LnBvd2VydXBDdXJzZU5hbWVUZXh0JwogICAgICAgICAgICAgICAgICAgIHRleCA9IGJhLmdldHRleHR1cmUoJ3Bvd2VydXBDdXJzZScpCiAgICAgICAgICAgICAgICBlbGlmIHBvd2VyID09IGlkX3Bvd2VyWzhdOgogICAgICAgICAgICAgICAgICAgIHRleHQgPSAnaGVscFdpbmRvdy5wb3dlcnVwSGVhbHRoTmFtZVRleHQnCiAgICAgICAgICAgICAgICAgICAgdGV4ID0gYmEuZ2V0dGV4dHVyZSgncG93ZXJ1cEhlYWx0aCcpCiAgICAgICAgICAgICAgICBlbGlmIHBvd2VyID09IGlkX3Bvd2VyWzldOgogICAgICAgICAgICAgICAgICAgIHRleHQgPSBwb3dlcgogICAgICAgICAgICAgICAgICAgIHRleCA9IGJhLmdldHRleHR1cmUoJ3Bvd2VydXBTcGVlZCcpCiAgICAgICAgICAgICAgICBlbGlmIHBvd2VyID09IGlkX3Bvd2VyWzEwXToKICAgICAgICAgICAgICAgICAgICB0ZXh0ID0gcG93ZXIKICAgICAgICAgICAgICAgICAgICB0ZXggPSBiYS5nZXR0ZXh0dXJlKCdoZWFydCcpCiAgICAgICAgICAgICAgICBlbGlmIHBvd2VyID09IGlkX3Bvd2VyWzExXToKICAgICAgICAgICAgICAgICAgICB0ZXh0ID0gIkdvb2RieWUhIgogICAgICAgICAgICAgICAgICAgIHRleCA9IGJhLmdldHRleHR1cmUoJ2FjaGlldmVtZW50T25zbGF1Z2h0JykKICAgICAgICAgICAgICAgIGVsaWYgcG93ZXIgPT0gaWRfcG93ZXJbMTJdOgogICAgICAgICAgICAgICAgICAgIHRleHQgPSBwb3dlcgogICAgICAgICAgICAgICAgICAgIHRleCA9IGJhLmdldHRleHR1cmUoJ291eWFVQnV0dG9uJykKICAgICAgICAgICAgICAgIGVsaWYgcG93ZXIgPT0gaWRfcG93ZXJbMTNdOgogICAgICAgICAgICAgICAgICAgIHRleHQgPSBwb3dlcgogICAgICAgICAgICAgICAgICAgIHRleCA9IGJhLmdldHRleHR1cmUoJ2FjaGlldmVtZW50U3VwZXJQdW5jaCcpCiAgICAgICAgICAgICAgICBlbGlmIHBvd2VyID09IGlkX3Bvd2VyWzE0XToKICAgICAgICAgICAgICAgICAgICB0ZXh0ID0gcG93ZXIKICAgICAgICAgICAgICAgICAgICB0ZXggPSBiYS5nZXR0ZXh0dXJlKCdsZXZlbEljb24nKQogICAgICAgICAgICAgICAgZWxpZiBwb3dlciA9PSBpZF9wb3dlclsxNV06CiAgICAgICAgICAgICAgICAgICAgdGV4dCA9IHBvd2VyCiAgICAgICAgICAgICAgICAgICAgdGV4ID0gYmEuZ2V0dGV4dHVyZSgnb3V5YU9CdXR0b24nKQogICAgICAgICAgICAgICAgZWxpZiBwb3dlciA9PSBpZF9wb3dlclsxNl06CiAgICAgICAgICAgICAgICAgICAgdGV4dCA9IHBvd2VyCiAgICAgICAgICAgICAgICAgICAgdGV4ID0gYmEuZ2V0dGV4dHVyZSgnc3RhcicpCiAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICBpZiBwb3dlciBpbiBuZXdfcG93ZXJ1cHM6IGxhYmVsID0gZ2V0bGFuZ3VhZ2UocG93ZXIpCiAgICAgICAgICAgICAgICBlbHNlOiBsYWJlbCA9IGJhLkxzdHIocmVzb3VyY2U9dGV4dCkKCiAgICAgICAgICAgICAgICBhcHBlcmFuY2UgPSBwb3dlcnVwc1twb3dlcl0KICAgICAgICAgICAgICAgIHBvc2l0aW9uID0gKDkwLHYtcG9zaSkKCiAgICAgICAgICAgICAgICB0ID0gYmEudGV4dHdpZGdldChwYXJlbnQ9Yyxwb3NpdGlvbj0ocG9zaXRpb25bMF0tMzAscG9zaXRpb25bMV0tMTUpLHNpemU9KHdpZHRoLDUwKSwKICAgICAgICAgICAgICAgICAgICAgICAgICBoX2FsaWduPSJjZW50ZXIiLGNvbG9yPShiYS5hcHAudWkudGl0bGVfY29sb3IpLCB0ZXh0PWxhYmVsLCB2X2FsaWduPSJjZW50ZXIiLG1heHdpZHRoPXdpZHRoKjEuMykKICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICBzZWxmLnBvd3ByZXYgPSBiYS5pbWFnZXdpZGdldChwYXJlbnQ9YywKICAgICAgICAgICAgICAgICAgICBwb3NpdGlvbj0ocG9zaXRpb25bMF0tNzAscG9zaXRpb25bMV0tMTApLAogICAgICAgICAgICAgICAgICAgIHNpemU9KDUwLDUwKSx0ZXh0dXJlPXRleCkKICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICBkaXBvcyA9IDAKICAgICAgICAgICAgICAgIGZvciBkaXJlYyBpbiBbJy0nLCcrJ106CiAgICAgICAgICAgICAgICAgICAgYmEuYnV0dG9ud2lkZ2V0KHBhcmVudD1jLGF1dG9zZWxlY3Q9VHJ1ZSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwb3NpdGlvbj0ocG9zaXRpb25bMF0rMjcwK2RpcG9zLHBvc2l0aW9uWzFdLTEwKSxzaXplPSgxMDAsMTAwKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzY2FsZT0wLjQsbGFiZWw9ZGlyZWMsYnV0dG9uX3R5cGU9J3NxdWFyZScsdGV4dF9zY2FsZT00LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG9uX2FjdGl2YXRlX2NhbGw9YmEuQ2FsbChzZWxmLmFwcGVyYW5jZV9wb3dlcnVwcyxwb3dlcixkaXJlYykpCiAgICAgICAgICAgICAgICAgICAgZGlwb3MgKz0gMTAwCiAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgdGV4dHdpZGdldCA9IGJhLnRleHR3aWRnZXQocGFyZW50PWMscG9zaXRpb249KHBvc2l0aW9uWzBdKzE5MCxwb3NpdGlvblsxXS0xNSksc2l6ZT0od2lkdGgsNTApLAogICAgICAgICAgICAgICAgICAgICAgICAgIGhfYWxpZ249ImNlbnRlciIsY29sb3I9Y2xzX3Bvd19jb2xvcigpW2FwcGVyYW5jZV0sdGV4dD1zdHIoYXBwZXJhbmNlKSwKICAgICAgICAgICAgICAgICAgICAgICAgICB2X2FsaWduPSJjZW50ZXIiLG1heHdpZHRoPXdpZHRoKjEuMykKICAgICAgICAgICAgICAgIHNlbGYubGlzdHBvd2VyW3Bvd2VyXSA9IHRleHR3aWRnZXQKICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICBwb3NpICs9IDkwCiAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgIGVsaWYgdGFiID09ICdBY3Rpb24gMic6CiAgICAgICAgICAgIHN1Yl9oZWlnaHQgPSAzNzAgaWYgbm90IFNUT1JFWydCdXkgT3B0aW9uJ10gZWxzZSA0NTAKICAgICAgICAgICAgdiA9IHN1Yl9oZWlnaHQgLSA1NQogICAgICAgICAgICB3aWR0aCA9IDMwMAogICAgICAgICAgICAKICAgICAgICAgICAgc2VsZi5fdGFiX2NvbnRhaW5lciA9IGMgPSBiYS5jb250YWluZXJ3aWRnZXQocGFyZW50PXNlbGYuX3Njcm9sbHdpZGdldCwKICAgICAgICAgICAgICAgIHNpemU9KHNlbGYuX3N1Yl93aWR0aCxzdWJfaGVpZ2h0KSwKICAgICAgICAgICAgICAgIGJhY2tncm91bmQ9RmFsc2Usc2VsZWN0aW9uX2xvb3BzX3RvX3BhcmVudD1UcnVlKQogICAgICAgICAgICAgICAKICAgICAgICAgICAgcG9zaXRpb24gPSAoNDAsdi0yMCkKICAgICAgICAgICAgICAgCiAgICAgICAgICAgIGNfZGlzcGxheSA9IFtdCiAgICAgICAgICAgIGNob2ljZXMgPSBbJ0F1dG8nLCdTWTogQkFMTCcsJ1NZOiBJbXBhY3QnLCdTWTogRWdnJ10KICAgICAgICAgICAgZm9yIGRpc3BsYXkgaW4gY2hvaWNlczoKICAgICAgICAgICAgICAgIGNob2ljZXNfZGlzcGxheSA9IGJhLkxzdHIodHJhbnNsYXRlPSgiIixnZXRsYW5ndWFnZShkaXNwbGF5KSkpCiAgICAgICAgICAgICAgICBjX2Rpc3BsYXkuYXBwZW5kKGNob2ljZXNfZGlzcGxheSkKICAgICAgICAgICAgICAgIAogICAgICAgICAgICBwb3B1cCA9IFBvcHVwTWVudShwYXJlbnQ9YywKICAgICAgICAgICAgICAgICAgcG9zaXRpb249KHBvc2l0aW9uWzBdKzMwMCxwb3NpdGlvblsxXSksCiAgICAgICAgICAgICAgICAgIGJ1dHRvbl9zaXplPSgxNTAsNTApLHNjYWxlPTIuNSwKICAgICAgICAgICAgICAgICAgY2hvaWNlcz1jaG9pY2VzLHdpZHRoPTE1MCwKICAgICAgICAgICAgICAgICAgY2hvaWNlc19kaXNwbGF5PWNfZGlzcGxheSwKICAgICAgICAgICAgICAgICAgY3VycmVudF9jaG9pY2U9Y29uZmlnWydQb3dlcnVwIFN0eWxlJ10sCiAgICAgICAgICAgICAgICAgIG9uX3ZhbHVlX2NoYW5nZV9jYWxsPWJhLkNhbGwoc2VsZi5fYWxsX3BvcHVwLCdQb3dlcnVwIFN0eWxlJykpCiAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICB0ZXh0ID0gZ2V0bGFuZ3VhZ2UoJ1Bvd2VydXAgU3R5bGUnKQogICAgICAgICAgICB3dCA9IChsZW4odGV4dCkqMC44MCkKICAgICAgICAgICAgdCA9IGJhLnRleHR3aWRnZXQocGFyZW50PWMscG9zaXRpb249KHBvc2l0aW9uWzBdLTYwK3d0LHBvc2l0aW9uWzFdKSxzaXplPSh3aWR0aCw1MCksbWF4d2lkdGg9d2lkdGgqMC45LAogICAgICAgICAgICAgICAgc2NhbGU9MS4xLGhfYWxpZ249ImNlbnRlciIsY29sb3I9YmEuYXBwLnVpLnRpdGxlX2NvbG9yLHRleHQ9Z2V0bGFuZ3VhZ2UoJ1Bvd2VydXAgU3R5bGUnKSx2X2FsaWduPSJjZW50ZXIiKQogICAgICAgICAgICAgICAgCiAgICAgICAgICAgIGRpcG9zID0gMAogICAgICAgICAgICBmb3IgZGlyZWMgaW4gWyctJywnKyddOgogICAgICAgICAgICAgICAgYmEuYnV0dG9ud2lkZ2V0KHBhcmVudD1jLGF1dG9zZWxlY3Q9VHJ1ZSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBvc2l0aW9uPShwb3NpdGlvblswXSszMTArZGlwb3MscG9zaXRpb25bMV0tMTAwKSxzaXplPSgxMDAsMTAwKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlcGVhdD1UcnVlLHNjYWxlPTAuNCxsYWJlbD1kaXJlYyxidXR0b25fdHlwZT0nc3F1YXJlJyx0ZXh0X3NjYWxlPTQsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBvbl9hY3RpdmF0ZV9jYWxsPWJhLkNhbGwoc2VsZi5fcG93ZXJ1cHNfc2NhbGUsZGlyZWMpKQogICAgICAgICAgICAgICAgZGlwb3MgKz0gMTAwCgogICAgICAgICAgICB0eHRfc2NhbGUgPSBjb25maWdbJ1Bvd2VydXAgU2NhbGUnXQogICAgICAgICAgICBzZWxmLnR4dF9zY2FsZSA9IGJhLnRleHR3aWRnZXQocGFyZW50PWMscG9zaXRpb249KHBvc2l0aW9uWzBdKzIzMCxwb3NpdGlvblsxXS0xMDUpLHNpemU9KHdpZHRoLDUwKSwKICAgICAgICAgICAgICAgICAgICAgICAgICBzY2FsZT0xLjEsaF9hbGlnbj0iY2VudGVyIixjb2xvcj0oMCwxLDApLHRleHQ9c3RyKHR4dF9zY2FsZSksdl9hbGlnbj0iY2VudGVyIixtYXh3aWR0aD13aWR0aCoxLjMpCiAgICAgICAgICAgICAKICAgICAgICAgICAgdGV4dCA9IGdldGxhbmd1YWdlKCdQb3dlcnVwIFNjYWxlJykKICAgICAgICAgICAgd3QgPSAobGVuKHRleHQpKjAuODApCiAgICAgICAgICAgIHQgPSBiYS50ZXh0d2lkZ2V0KHBhcmVudD1jLHBvc2l0aW9uPShwb3NpdGlvblswXS02MCt3dCxwb3NpdGlvblsxXS0xMDApLHNpemU9KHdpZHRoLDUwKSxtYXh3aWR0aD13aWR0aCowLjksCiAgICAgICAgICAgICAgICBzY2FsZT0xLjEsaF9hbGlnbj0iY2VudGVyIixjb2xvcj1iYS5hcHAudWkudGl0bGVfY29sb3IsdGV4dD10ZXh0LHZfYWxpZ249ImNlbnRlciIpCiAgICAgICAgICAgICAKICAgICAgICAgICAgcG9zaXRpb24gPSAocG9zaXRpb25bMF0tMjAscG9zaXRpb25bMV0rNDApCiAgICAgICAgICAgICAgICAKICAgICAgICAgICAgc2VsZi5jaGVjayA9IGJhLmNoZWNrYm94d2lkZ2V0KHBhcmVudD1jLHBvc2l0aW9uPShwb3NpdGlvblswXSszMCxwb3NpdGlvblsxXS0yMzApLHZhbHVlPWNvbmZpZ1snUG93ZXJ1cCBOYW1lJ10sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgb25fdmFsdWVfY2hhbmdlX2NhbGw9YmEuQ2FsbChzZWxmLl9zd2l0Y2hlcywnUG93ZXJ1cCBOYW1lJyksbWF4d2lkdGg9c2VsZi5fc2Nyb2xsX3dpZHRoKjAuOSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0ZXh0PWdldGxhbmd1YWdlKCdQb3dlcnVwIE5hbWUnKSxhdXRvc2VsZWN0PVRydWUpCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgIHNlbGYuY2hlY2sgPSBiYS5jaGVja2JveHdpZGdldChwYXJlbnQ9Yyxwb3NpdGlvbj0ocG9zaXRpb25bMF0rMzAscG9zaXRpb25bMV0tMjMwKjEuMyksdmFsdWU9Y29uZmlnWydQb3dlcnVwIFdpdGggU2hpZWxkJ10sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgb25fdmFsdWVfY2hhbmdlX2NhbGw9YmEuQ2FsbChzZWxmLl9zd2l0Y2hlcywnUG93ZXJ1cCBXaXRoIFNoaWVsZCcpLG1heHdpZHRoPXNlbGYuX3Njcm9sbF93aWR0aCowLjksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdGV4dD1nZXRsYW5ndWFnZSgnUG93ZXJ1cCBXaXRoIFNoaWVsZCcpLGF1dG9zZWxlY3Q9VHJ1ZSkKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgaWYgU1RPUkVbJ0J1eSBPcHRpb24nXToKICAgICAgICAgICAgICAgIHNlbGYuY2hlY2sgPSBiYS5jaGVja2JveHdpZGdldChwYXJlbnQ9Yyxwb3NpdGlvbj0ocG9zaXRpb25bMF0rMzAscG9zaXRpb25bMV0tMjMwKjEuNiksdmFsdWU9Y29uZmlnWydQb3dlcnVwIFRpbWUnXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgb25fdmFsdWVfY2hhbmdlX2NhbGw9YmEuQ2FsbChzZWxmLl9zd2l0Y2hlcywnUG93ZXJ1cCBUaW1lJyksbWF4d2lkdGg9c2VsZi5fc2Nyb2xsX3dpZHRoKjAuOSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdGV4dD1nZXRsYW5ndWFnZSgnUG93ZXJ1cCBUaW1lJyksYXV0b3NlbGVjdD1UcnVlKQogICAgICAgICAgICAgICAgCiAgICAgICAgZWxpZiB0YWIgPT0gJ0FjdGlvbiAzJzoKICAgICAgICAgICAgc3ViX2hlaWdodCA9IDMwMAogICAgICAgICAgICB2ID0gc3ViX2hlaWdodCAtIDU1CiAgICAgICAgICAgIHdpZHRoID0gMzAwCiAgICAgICAgICAgIAogICAgICAgICAgICBzZWxmLl90YWJfY29udGFpbmVyID0gYyA9IGJhLmNvbnRhaW5lcndpZGdldChwYXJlbnQ9c2VsZi5fc2Nyb2xsd2lkZ2V0LAogICAgICAgICAgICAgICAgc2l6ZT0oc2VsZi5fc3ViX3dpZHRoLHN1Yl9oZWlnaHQpLAogICAgICAgICAgICAgICAgYmFja2dyb3VuZD1GYWxzZSxzZWxlY3Rpb25fbG9vcHNfdG9fcGFyZW50PVRydWUpCgogICAgICAgICAgICB2IC09IDIwCiAgICAgICAgICAgIHBvc2l0aW9uID0gKDExMCx2LTQ1KjEuNzIpCiAgICAgICAgICAgIAogICAgICAgICAgICBpZiBub3QgU1RPUkVbJ0J1eSBQZXJjZW50YWdlJ106CiAgICAgICAgICAgICAgICB0ID0gYmEudGV4dHdpZGdldChwYXJlbnQ9Yyxwb3NpdGlvbj0oOTAsdi0xMDApLHNpemU9KDMwK3dpZHRoLDUwKSwKICAgICAgICAgICAgICAgICAgICBoX2FsaWduPSJjZW50ZXIiLHRleHQ9Z2V0bGFuZ3VhZ2UoJ0Jsb2NrIE9wdGlvbiBTdG9yZScpLAogICAgICAgICAgICAgICAgICAgIGNvbG9yPWJhLmFwcC51aS50aXRsZV9jb2xvcix2X2FsaWduPSJjZW50ZXIiLG1heHdpZHRoPXdpZHRoKjEuNSxzY2FsZT0xLjUpCiAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICBpID0gYmEuaW1hZ2V3aWRnZXQocGFyZW50PWMsCiAgICAgICAgICAgICAgICAgICAgcG9zaXRpb249KHBvc2l0aW9uWzBdKzEwMCxwb3NpdGlvblsxXS0yMDUpLAogICAgICAgICAgICAgICAgICAgIHNpemU9KDgwLDgwKSx0ZXh0dXJlPWJhLmdldHRleHR1cmUoJ2xvY2snKSkKICAgICAgICAgICAgZWxzZToKICAgICAgICAgICAgICAgIHQgPSBiYS50ZXh0d2lkZ2V0KHBhcmVudD1jLHBvc2l0aW9uPShwb3NpdGlvblswXS0xNCxwb3NpdGlvblsxXSs3MCksc2l6ZT0oMzArd2lkdGgsNTApLAogICAgICAgICAgICAgICAgICAgIGhfYWxpZ249ImNlbnRlciIsdGV4dD1mIntnZXRsYW5ndWFnZSgnVGFuayBTaGllbGQgUFRHJyl9ICh7Z2V0bGFuZ3VhZ2UoJ1RhbmsgU2hpZWxkJyl9KSIsCiAgICAgICAgICAgICAgICAgICAgY29sb3I9YmEuYXBwLnVpLnRpdGxlX2NvbG9yLHZfYWxpZ249ImNlbnRlciIsbWF4d2lkdGg9d2lkdGgqMS41LHNjYWxlPTEuNSkKICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgYiA9IGJhLmJ1dHRvbndpZGdldChwYXJlbnQ9YyxhdXRvc2VsZWN0PVRydWUscG9zaXRpb249cG9zaXRpb24sc2l6ZT0oMTAwLDEwMCkscmVwZWF0PVRydWUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNjYWxlPTAuNixsYWJlbD1zZWxmLmNoYXJzdHJbM10sYnV0dG9uX3R5cGU9J3NxdWFyZScsdGV4dF9zY2FsZT0yLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBvbl9hY3RpdmF0ZV9jYWxsPWJhLkNhbGwoc2VsZi50YW5rX3NoaWVsZF9wZXJjZW50YWdlLCdEZWNyZW1lbnQnKSkKICAgIAogICAgICAgICAgICAgICAgYiA9IGJhLmJ1dHRvbndpZGdldChwYXJlbnQ9YyxhdXRvc2VsZWN0PVRydWUscmVwZWF0PVRydWUsdGV4dF9zY2FsZT0yLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwb3NpdGlvbj0ocG9zaXRpb25bMF0qMy4yLHBvc2l0aW9uWzFdKSxzaXplPSgxMDAsMTAwKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2NhbGU9MC42LGxhYmVsPXNlbGYuY2hhcnN0clsyXSxidXR0b25fdHlwZT0nc3F1YXJlJywKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgb25fYWN0aXZhdGVfY2FsbD1iYS5DYWxsKHNlbGYudGFua19zaGllbGRfcGVyY2VudGFnZSwnSW5jcmVtZW50JykpCiAgICAKICAgICAgICAgICAgICAgIHBvcmNlbnRhamUgPSBjb25maWdbJ1RhbmsgU2hpZWxkIFBURyddCiAgICAgICAgICAgICAgICBpZiBwb3JjZW50YWplID4gNTk6IGNvbG9yID0gKDAsMSwwKQogICAgICAgICAgICAgICAgZWxpZiBwb3JjZW50YWplIDwgNDA6IGNvbG9yID0gKDEsMSwwKQogICAgICAgICAgICAgICAgZWxzZTogY29sb3IgPSAoMCwxLDAuOCkKICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgc2VsZi50YW5rX3RleHQgPSBiYS50ZXh0d2lkZ2V0KHBhcmVudD1jLHBvc2l0aW9uPShwb3NpdGlvblswXS0xNCxwb3NpdGlvblsxXSs1KSwKICAgICAgICAgICAgICAgICAgICBzaXplPSgzMCt3aWR0aCw1MCksaF9hbGlnbj0iY2VudGVyIiwKICAgICAgICAgICAgICAgICAgICB0ZXh0PXN0cihwb3JjZW50YWplKSsnJScsY29sb3I9Y29sb3IsCiAgICAgICAgICAgICAgICAgICAgdl9hbGlnbj0iY2VudGVyIixtYXh3aWR0aD13aWR0aCoxLjMsc2NhbGU9MikKICAgIAogICAgICAgICAgICAgICAgIyAtLS0tLT4KICAgIAogICAgICAgICAgICAgICAgcG9zaXRpb24gPSAoMTEwLHYtMTYwKjEuNikgICAgICAgICAKICAgICAgICAgICAgICAgIHQgPSBiYS50ZXh0d2lkZ2V0KHBhcmVudD1jLHBvc2l0aW9uPShwb3NpdGlvblswXS0xNCxwb3NpdGlvblsxXSs3MCksc2l6ZT0oMzArd2lkdGgsNTApLAogICAgICAgICAgICAgICAgICAgIGhfYWxpZ249ImNlbnRlciIsdGV4dD1mIntnZXRsYW5ndWFnZSgnSGVhbGluZyBEYW1hZ2UgUFRHJyl9e19zcF99KHtnZXRsYW5ndWFnZSgnSGVhbGluZyBEYW1hZ2UnKX0pIiwKICAgICAgICAgICAgICAgICAgICBjb2xvcj1iYS5hcHAudWkudGl0bGVfY29sb3Isdl9hbGlnbj0iY2VudGVyIixtYXh3aWR0aD13aWR0aCoxLjMsc2NhbGU9MS40KQogICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICBiID0gYmEuYnV0dG9ud2lkZ2V0KHBhcmVudD1jLGF1dG9zZWxlY3Q9VHJ1ZSxwb3NpdGlvbj1wb3NpdGlvbixzaXplPSgxMDAsMTAwKSxyZXBlYXQ9VHJ1ZSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2NhbGU9MC42LGxhYmVsPXNlbGYuY2hhcnN0clszXSxidXR0b25fdHlwZT0nc3F1YXJlJyx0ZXh0X3NjYWxlPTIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG9uX2FjdGl2YXRlX2NhbGw9YmEuQ2FsbChzZWxmLmhlYWx0aF9kYW1hZ2VfcGVyY2VudGFnZSwnRGVjcmVtZW50JykpCiAgICAKICAgICAgICAgICAgICAgIGIgPSBiYS5idXR0b253aWRnZXQocGFyZW50PWMsYXV0b3NlbGVjdD1UcnVlLHJlcGVhdD1UcnVlLHRleHRfc2NhbGU9MiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcG9zaXRpb249KHBvc2l0aW9uWzBdKjMuMixwb3NpdGlvblsxXSksc2l6ZT0oMTAwLDEwMCksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNjYWxlPTAuNixsYWJlbD1zZWxmLmNoYXJzdHJbMl0sYnV0dG9uX3R5cGU9J3NxdWFyZScsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG9uX2FjdGl2YXRlX2NhbGw9YmEuQ2FsbChzZWxmLmhlYWx0aF9kYW1hZ2VfcGVyY2VudGFnZSwnSW5jcmVtZW50JykpCiAgICAKICAgICAgICAgICAgICAgIHBvcmNlbnRhamUgPSBjb25maWdbJ0hlYWxpbmcgRGFtYWdlIFBURyddCiAgICAgICAgICAgICAgICBpZiBwb3JjZW50YWplID4gNTk6IGNvbG9yID0gKDAsMSwwKQogICAgICAgICAgICAgICAgZWxpZiBwb3JjZW50YWplIDwgNDA6IGNvbG9yID0gKDEsMSwwKQogICAgICAgICAgICAgICAgZWxzZTogY29sb3IgPSAoMCwxLDAuOCkKICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgc2VsZi5obGdfdGV4dCA9IGJhLnRleHR3aWRnZXQocGFyZW50PWMscG9zaXRpb249KHBvc2l0aW9uWzBdLTE0LHBvc2l0aW9uWzFdKzUpLAogICAgICAgICAgICAgICAgICAgIHNpemU9KDMwK3dpZHRoLDUwKSxoX2FsaWduPSJjZW50ZXIiLAogICAgICAgICAgICAgICAgICAgIHRleHQ9c3RyKHBvcmNlbnRhamUpKyclJyxjb2xvcj1jb2xvciwKICAgICAgICAgICAgICAgICAgICB2X2FsaWduPSJjZW50ZXIiLG1heHdpZHRoPXdpZHRoKjEuMyxzY2FsZT0yKQoKICAgICAgICBlbGlmIHRhYiA9PSAnUGVyY2VudGFnZSc6CiAgICAgICAgICAgIHN1Yl9oZWlnaHQgPSBsZW4oc2VsZi5kZWZhdWx0X3Bvd2VyX2xpc3QpICogOTAKICAgICAgICAgICAgdiA9IHN1Yl9oZWlnaHQgLSA1NQogICAgICAgICAgICB3aWR0aCA9IDMwMAogICAgICAgICAgICBwb3NpID0gMAogICAgICAgICAgICBpZF9wb3dlciA9IGxpc3Qoc2VsZi5kZWZhdWx0X3Bvd2VydXBzKQogICAgICAgICAgICBuZXdfcG93ZXJ1cHMgPSBpZF9wb3dlcls5Ol0KICAgICAgICAgICAgc2VsZi5saXN0cG93ZXIgPSB7fQogICAgICAgICAgICAKICAgICAgICAgICAgc2VsZi5fdGFiX2NvbnRhaW5lciA9IGMgPSBiYS5jb250YWluZXJ3aWRnZXQocGFyZW50PXNlbGYuX3Njcm9sbHdpZGdldCwKICAgICAgICAgICAgICAgIHNpemU9KHNlbGYuX3N1Yl93aWR0aCxzdWJfaGVpZ2h0KSwKICAgICAgICAgICAgICAgIGJhY2tncm91bmQ9RmFsc2Usc2VsZWN0aW9uX2xvb3BzX3RvX3BhcmVudD1UcnVlKQogCiAgICAgICAgICAgIGZvciBwb3dlciBpbiBzZWxmLmRlZmF1bHRfcG93ZXJfbGlzdDoKICAgICAgICAgICAgICAgIGlmIHBvd2VyID09IGlkX3Bvd2VyWzBdOgogICAgICAgICAgICAgICAgICAgIHRleHQgPSAnaGVscFdpbmRvdy5wb3dlcnVwU2hpZWxkTmFtZVRleHQnCiAgICAgICAgICAgICAgICAgICAgdGV4ID0gYmEuZ2V0dGV4dHVyZSgncG93ZXJ1cFNoaWVsZCcpCiAgICAgICAgICAgICAgICBlbGlmIHBvd2VyID09IGlkX3Bvd2VyWzFdOgogICAgICAgICAgICAgICAgICAgIHRleHQgPSAnaGVscFdpbmRvdy5wb3dlcnVwUHVuY2hOYW1lVGV4dCcKICAgICAgICAgICAgICAgICAgICB0ZXggPSBiYS5nZXR0ZXh0dXJlKCdwb3dlcnVwUHVuY2gnKQogICAgICAgICAgICAgICAgZWxpZiBwb3dlciA9PSBpZF9wb3dlclsyXToKICAgICAgICAgICAgICAgICAgICB0ZXh0ID0gJ2hlbHBXaW5kb3cucG93ZXJ1cExhbmRNaW5lc05hbWVUZXh0JwogICAgICAgICAgICAgICAgICAgIHRleCA9IGJhLmdldHRleHR1cmUoJ3Bvd2VydXBMYW5kTWluZXMnKQogICAgICAgICAgICAgICAgZWxpZiBwb3dlciA9PSBpZF9wb3dlclszXToKICAgICAgICAgICAgICAgICAgICB0ZXh0ID0gJ2hlbHBXaW5kb3cucG93ZXJ1cEltcGFjdEJvbWJzTmFtZVRleHQnCiAgICAgICAgICAgICAgICAgICAgdGV4ID0gYmEuZ2V0dGV4dHVyZSgncG93ZXJ1cEltcGFjdEJvbWJzJykKICAgICAgICAgICAgICAgIGVsaWYgcG93ZXIgPT0gaWRfcG93ZXJbNF06CiAgICAgICAgICAgICAgICAgICAgdGV4dCA9ICdoZWxwV2luZG93LnBvd2VydXBJY2VCb21ic05hbWVUZXh0JwogICAgICAgICAgICAgICAgICAgIHRleCA9IGJhLmdldHRleHR1cmUoJ3Bvd2VydXBJY2VCb21icycpCiAgICAgICAgICAgICAgICBlbGlmIHBvd2VyID09IGlkX3Bvd2VyWzVdOgogICAgICAgICAgICAgICAgICAgIHRleHQgPSAnaGVscFdpbmRvdy5wb3dlcnVwQm9tYk5hbWVUZXh0JwogICAgICAgICAgICAgICAgICAgIHRleCA9IGJhLmdldHRleHR1cmUoJ3Bvd2VydXBCb21iJykKICAgICAgICAgICAgICAgIGVsaWYgcG93ZXIgPT0gaWRfcG93ZXJbNl06CiAgICAgICAgICAgICAgICAgICAgdGV4dCA9ICdoZWxwV2luZG93LnBvd2VydXBTdGlja3lCb21ic05hbWVUZXh0JwogICAgICAgICAgICAgICAgICAgIHRleCA9IGJhLmdldHRleHR1cmUoJ3Bvd2VydXBTdGlja3lCb21icycpCiAgICAgICAgICAgICAgICBlbGlmIHBvd2VyID09IGlkX3Bvd2VyWzddOgogICAgICAgICAgICAgICAgICAgIHRleHQgPSAnaGVscFdpbmRvdy5wb3dlcnVwQ3Vyc2VOYW1lVGV4dCcKICAgICAgICAgICAgICAgICAgICB0ZXggPSBiYS5nZXR0ZXh0dXJlKCdwb3dlcnVwQ3Vyc2UnKQogICAgICAgICAgICAgICAgZWxpZiBwb3dlciA9PSBpZF9wb3dlcls4XToKICAgICAgICAgICAgICAgICAgICB0ZXh0ID0gJ2hlbHBXaW5kb3cucG93ZXJ1cEhlYWx0aE5hbWVUZXh0JwogICAgICAgICAgICAgICAgICAgIHRleCA9IGJhLmdldHRleHR1cmUoJ3Bvd2VydXBIZWFsdGgnKQogICAgICAgICAgICAgICAgZWxpZiBwb3dlciA9PSBpZF9wb3dlcls5XToKICAgICAgICAgICAgICAgICAgICB0ZXh0ID0gcG93ZXIKICAgICAgICAgICAgICAgICAgICB0ZXggPSBiYS5nZXR0ZXh0dXJlKCdwb3dlcnVwU3BlZWQnKQogICAgICAgICAgICAgICAgZWxpZiBwb3dlciA9PSBpZF9wb3dlclsxMF06CiAgICAgICAgICAgICAgICAgICAgdGV4dCA9IHBvd2VyCiAgICAgICAgICAgICAgICAgICAgdGV4ID0gYmEuZ2V0dGV4dHVyZSgnaGVhcnQnKQogICAgICAgICAgICAgICAgZWxpZiBwb3dlciA9PSBpZF9wb3dlclsxMV06CiAgICAgICAgICAgICAgICAgICAgdGV4dCA9ICJHb29kYnllISIKICAgICAgICAgICAgICAgICAgICB0ZXggPSBiYS5nZXR0ZXh0dXJlKCdhY2hpZXZlbWVudE9uc2xhdWdodCcpCiAgICAgICAgICAgICAgICBlbGlmIHBvd2VyID09IGlkX3Bvd2VyWzEyXToKICAgICAgICAgICAgICAgICAgICB0ZXh0ID0gcG93ZXIKICAgICAgICAgICAgICAgICAgICB0ZXggPSBiYS5nZXR0ZXh0dXJlKCdvdXlhVUJ1dHRvbicpCiAgICAgICAgICAgICAgICBlbGlmIHBvd2VyID09IGlkX3Bvd2VyWzEzXToKICAgICAgICAgICAgICAgICAgICB0ZXh0ID0gcG93ZXIKICAgICAgICAgICAgICAgICAgICB0ZXggPSBiYS5nZXR0ZXh0dXJlKCdhY2hpZXZlbWVudFN1cGVyUHVuY2gnKQogICAgICAgICAgICAgICAgZWxpZiBwb3dlciA9PSBpZF9wb3dlclsxNF06CiAgICAgICAgICAgICAgICAgICAgdGV4dCA9IHBvd2VyCiAgICAgICAgICAgICAgICAgICAgdGV4ID0gYmEuZ2V0dGV4dHVyZSgnbGV2ZWxJY29uJykKICAgICAgICAgICAgICAgIGVsaWYgcG93ZXIgPT0gaWRfcG93ZXJbMTVdOgogICAgICAgICAgICAgICAgICAgIHRleHQgPSBwb3dlcgogICAgICAgICAgICAgICAgICAgIHRleCA9IGJhLmdldHRleHR1cmUoJ291eWFPQnV0dG9uJykKICAgICAgICAgICAgICAgIGVsaWYgcG93ZXIgPT0gaWRfcG93ZXJbMTZdOgogICAgICAgICAgICAgICAgICAgIHRleHQgPSBwb3dlcgogICAgICAgICAgICAgICAgICAgIHRleCA9IGJhLmdldHRleHR1cmUoJ3N0YXInKQogICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgaWYgcG93ZXIgaW4gbmV3X3Bvd2VydXBzOiBsYWJlbCA9IGdldGxhbmd1YWdlKHBvd2VyKQogICAgICAgICAgICAgICAgZWxzZTogbGFiZWwgPSBiYS5Mc3RyKHJlc291cmNlPXRleHQpCgogICAgICAgICAgICAgICAgYXBwZXJhbmNlID0gcG93ZXJ1cHNbcG93ZXJdCiAgICAgICAgICAgICAgICBwb3NpdGlvbiA9ICg5MCx2LXBvc2kpCiAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgIHQgPSBiYS50ZXh0d2lkZ2V0KHBhcmVudD1jLHBvc2l0aW9uPShwb3NpdGlvblswXS0zMCxwb3NpdGlvblsxXS0xNSksc2l6ZT0od2lkdGgsNTApLAogICAgICAgICAgICAgICAgICAgICAgICAgIGhfYWxpZ249ImNlbnRlciIsY29sb3I9KGJhLmFwcC51aS50aXRsZV9jb2xvciksIHRleHQ9bGFiZWwsIHZfYWxpZ249ImNlbnRlciIsbWF4d2lkdGg9d2lkdGgqMS4zKQogICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgIHNlbGYucG93cHJldiA9IGJhLmltYWdld2lkZ2V0KHBhcmVudD1jLAogICAgICAgICAgICAgICAgICAgIHBvc2l0aW9uPShwb3NpdGlvblswXS03MCxwb3NpdGlvblsxXS0xMCksCiAgICAgICAgICAgICAgICAgICAgc2l6ZT0oNTAsNTApLHRleHR1cmU9dGV4KQogICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgIHB0ZyA9IHN0cihzZWxmLnRvdGFsX3BlcmNlbnRhZ2UocG93ZXIpKQogICAgICAgICAgICAgICAgdCA9IGJhLnRleHR3aWRnZXQocGFyZW50PWMscG9zaXRpb249KHBvc2l0aW9uWzBdKzE3MCxwb3NpdGlvblsxXS0xMCksc2l6ZT0od2lkdGgsNTApLAogICAgICAgICAgICAgICAgICAgIGhfYWxpZ249ImNlbnRlciIsY29sb3I9KDAsMSwwKSx0ZXh0PShmJ3twdGd9JScpLHZfYWxpZ249ImNlbnRlciIsbWF4d2lkdGg9d2lkdGgqMS4zKQogICAgICAgICAKICAgICAgICAgICAgICAgIHBvc2kgKz0gOTAKICAgICAgICAgICAgICAgIAogICAgICAgIGVsaWYgdGFiID09ICdBY3Rpb24gNCc6CiAgICAgICAgICAgIHN1Yl9oZWlnaHQgPSAzNzAKICAgICAgICAgICAgd2lkdGggPSAzMDAKICAgICAgICAgICAgdiA9IHN1Yl9oZWlnaHQgLSA1NQogICAgICAgICAgICB1ID0gd2lkdGggLSA2MAogICAgICAgICAgICAKICAgICAgICAgICAgc2VsZi5fdGFiX2NvbnRhaW5lciA9IGMgPSBiYS5jb250YWluZXJ3aWRnZXQocGFyZW50PXNlbGYuX3Njcm9sbHdpZGdldCwKICAgICAgICAgICAgICAgIHNpemU9KHdpZHRoKzUwMCxzdWJfaGVpZ2h0KSwKICAgICAgICAgICAgICAgIGJhY2tncm91bmQ9RmFsc2Usc2VsZWN0aW9uX2xvb3BzX3RvX3BhcmVudD1UcnVlKQogICAgICAgICAgICAgICAKICAgICAgICAgICAgcG9zaXRpb24gPSAodSsxNTAsdi0yNTApCiAgICAgICAgICAgIG5fcG9zID0gMAogICAgICAgICAgICBwcmljZXMgPSBbNzU2MCwgNTE1MCwgMzM2MF0KICAgICAgICAgICAgc3RyX25hbWUgPSBbIkZpcmVCb21icyBTdG9yZSIsIlRpbWVyIFN0b3JlIiwiUGVyY2VudGFnZXMgU3RvcmUiXQogICAgICAgICAgICBpbWFnZXMgPSBbIm91eWFPQnV0dG9uIiwic2V0dGluZ3NJY29uIiwiaW52ZW50b3J5SWNvbiJdCiAgICAgICAgICAgIAogICAgICAgICAgICBpbmRleCA9IDAKICAgICAgICAgICAgZm9yIHN0b3JlIGluIHN0b3JlX2l0ZW1zKCk6CiAgICAgICAgICAgICAgICBwID0gcHJpY2VzW2luZGV4XQogICAgICAgICAgICAgICAgdHh0ID0gc3RyX25hbWVbaW5kZXhdCiAgICAgICAgICAgICAgICBsYWJlbCA9IGdldGxhbmd1YWdlKHR4dCkKICAgICAgICAgICAgICAgIHR4X3BvcyA9IGxlbihsYWJlbCkqMS44CiAgICAgICAgICAgICAgICBsYl9zY2FsZSA9IGxlbihsYWJlbCkqMC4yMAogICAgICAgICAgICAgICAgcHJldmlldyA9IGltYWdlc1tpbmRleF0KICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgaWYgU1RPUkVbc3RvcmVdOgogICAgICAgICAgICAgICAgICAgIHRleHQgPSBnZXRsYW5ndWFnZSgnQm91Z2h0JykKICAgICAgICAgICAgICAgICAgICBpY29uID0gYmEuZ2V0dGV4dHVyZSgnZ3JhcGhpY3NJY29uJykKICAgICAgICAgICAgICAgICAgICBjb2xvciA9ICgwLjUyLDAuNDgsMC42MykKICAgICAgICAgICAgICAgICAgICB0eHRfc2NhbGUgPSAxLjUKICAgICAgICAgICAgICAgIGVsc2U6CiAgICAgICAgICAgICAgICAgICAgdGV4dCA9IHN0cihwKQogICAgICAgICAgICAgICAgICAgIGljb24gPSBiYS5nZXR0ZXh0dXJlKCdjb2luJykKICAgICAgICAgICAgICAgICAgICBjb2xvciA9ICgwLjUsMC40LDAuOTMpCiAgICAgICAgICAgICAgICAgICAgdHh0X3NjYWxlID0gMgogICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICBiID0gYmEuYnV0dG9ud2lkZ2V0KHBhcmVudD1jLGF1dG9zZWxlY3Q9VHJ1ZSxwb3NpdGlvbj0ocG9zaXRpb25bMF0rMjEwLW5fcG9zLHBvc2l0aW9uWzFdKSwKICAgICAgICAgICAgICAgICAgICAgICAgc2l6ZT0oMjUwLDgwKSxzY2FsZT0wLjcsbGFiZWw9dGV4dCx0ZXh0X3NjYWxlPXR4dF9zY2FsZSxpY29uPWljb24sY29sb3I9Y29sb3IsCiAgICAgICAgICAgICAgICAgICAgICAgIGljb25zY2FsZT0xLjcsb25fYWN0aXZhdGVfY2FsbD1iYS5DYWxsKHNlbGYuX2J1eV9vYmplY3Qsc3RvcmUscCkpCgogICAgICAgICAgICAgICAgcyA9IDE4MAogICAgICAgICAgICAgICAgYiA9IGJhLmJ1dHRvbndpZGdldChwYXJlbnQ9YyxhdXRvc2VsZWN0PVRydWUscG9zaXRpb249KHBvc2l0aW9uWzBdKzIxMC1uX3Bvcyxwb3NpdGlvblsxXSs1NSksCiAgICAgICAgICAgICAgICAgICAgICAgIHNpemU9KHMscyszMCksc2NhbGU9MSxsYWJlbD0nJyxjb2xvcj1jb2xvcixidXR0b25fdHlwZT0nc3F1YXJlJywKICAgICAgICAgICAgICAgICAgICAgICAgb25fYWN0aXZhdGVfY2FsbD1iYS5DYWxsKHNlbGYuX2J1eV9vYmplY3Qsc3RvcmUscCkpCiAgICAKICAgICAgICAgICAgICAgIHMgLT0gODAKICAgICAgICAgICAgICAgIGkgPSBiYS5pbWFnZXdpZGdldChwYXJlbnQ9YyxkcmF3X2NvbnRyb2xsZXI9YiwKICAgICAgICAgICAgICAgICAgICBwb3NpdGlvbj0ocG9zaXRpb25bMF0rMjUwLW5fcG9zLHBvc2l0aW9uWzFdKzE0MCksCiAgICAgICAgICAgICAgICAgICAgc2l6ZT0ocyxzKSx0ZXh0dXJlPWJhLmdldHRleHR1cmUocHJldmlldykpCiAgICAKICAgICAgICAgICAgICAgIHQgPSBiYS50ZXh0d2lkZ2V0KHBhcmVudD1jLHBvc2l0aW9uPShwb3NpdGlvblswXSsyNzAtbl9wb3MscG9zaXRpb25bMV0rMTAxKSwKICAgICAgICAgICAgICAgICAgICBoX2FsaWduPSJjZW50ZXIiLGNvbG9yPShiYS5hcHAudWkudGl0bGVfY29sb3IpLHRleHQ9bGFiZWwsdl9hbGlnbj0iY2VudGVyIixtYXh3aWR0aD0xMzApCiAgICAKICAgICAgICAgICAgICAgIG5fcG9zICs9IDI4MAogICAgICAgICAgICAgICAgaW5kZXggKz0gMQogICAgCiAgICAgICAgZWxpZiB0YWIgPT0gJ0FjdGlvbiA1JzoKICAgICAgICAgICAgc3ViX2hlaWdodCA9IDM3MAogICAgICAgICAgICB2ID0gc3ViX2hlaWdodCAtIDU1CiAgICAgICAgICAgIHdpZHRoID0gMzAwCiAgICAgICAgICAgIAogICAgICAgICAgICBzZWxmLl90YWJfY29udGFpbmVyID0gYyA9IGJhLmNvbnRhaW5lcndpZGdldChwYXJlbnQ9c2VsZi5fc2Nyb2xsd2lkZ2V0LAogICAgICAgICAgICAgICAgc2l6ZT0oc2VsZi5fc3ViX3dpZHRoLHN1Yl9oZWlnaHQpLGJhY2tncm91bmQ9RmFsc2UsCiAgICAgICAgICAgICAgICBzZWxlY3Rpb25fbG9vcHNfdG9fcGFyZW50PVRydWUpCiAgICAgICAgICAgICAgIAogICAgICAgICAgICBwb3NpdGlvbiA9ICgwLHYtMzApCiAgICAKICAgICAgICAgICAgdCA9IGJhLnRleHR3aWRnZXQocGFyZW50PWMscG9zaXRpb249KHBvc2l0aW9uWzBdKzgwLHBvc2l0aW9uWzFdLTMwKSxzaXplPSh3aWR0aCs2MCw1MCksc2NhbGU9MSwKICAgICAgICAgICAgICAgIGhfYWxpZ249ImNlbnRlciIsY29sb3I9KGJhLmFwcC51aS50aXRsZV9jb2xvciksdGV4dD1iYS5Mc3RyKAogICAgICAgICAgICAgICAgcmVzb3VyY2U9J3NldHRpbmdzV2luZG93QWR2YW5jZWQuZW50ZXJQcm9tb0NvZGVUZXh0Jyksdl9hbGlnbj0iY2VudGVyIixtYXh3aWR0aD13aWR0aCoxLjMpCiAgICAKICAgICAgICAgICAgc2VsZi5wcm9tb2NvZGVfdGV4dCA9IGJhLnRleHR3aWRnZXQocGFyZW50PWMscG9zaXRpb249KHBvc2l0aW9uWzBdKzgwLHBvc2l0aW9uWzFdLTEwMCksc2l6ZT0od2lkdGgrNjAsNTApLHNjYWxlPTEsCiAgICAgICAgICAgICAgICBlZGl0YWJsZT1UcnVlLGhfYWxpZ249ImNlbnRlciIsY29sb3I9KGJhLmFwcC51aS50aXRsZV9jb2xvciksdGV4dD0nJywgdl9hbGlnbj0iY2VudGVyIixtYXh3aWR0aD13aWR0aCoxLjMsCiAgICAgICAgICAgICAgICBtYXhfY2hhcnM9MzAsZGVzY3JpcHRpb249YmEuTHN0cihyZXNvdXJjZT0nc2V0dGluZ3NXaW5kb3dBZHZhbmNlZC5lbnRlclByb21vQ29kZVRleHQnKSkKICAgICAgICAgICAgICAgIAogICAgICAgICAgICBzZWxmLnByb21vY29kZV9idXR0b24gPSBiYS5idXR0b253aWRnZXQoCiAgICAgICAgICAgICAgICBwYXJlbnQ9Yyxwb3NpdGlvbj0ocG9zaXRpb25bMF0rMTYwLHBvc2l0aW9uWzFdLTE3MCksCiAgICAgICAgICAgICAgICBzaXplPSgyMDAsIDYwKSxzY2FsZT0xLjAsbGFiZWw9YmEuTHN0cihyZXNvdXJjZT0nc3VibWl0VGV4dCcpLAogICAgICAgICAgICAgICAgb25fYWN0aXZhdGVfY2FsbD1zZWxmLl9wcm9tb2NvZGUpCiAgICAKICAgICAgICBlbHNlOgogICAgICAgICAgICBzdWJfaGVpZ2h0ID0gMAogICAgICAgICAgICB2ID0gc3ViX2hlaWdodCAtIDU1CiAgICAgICAgICAgIHdpZHRoID0gMzAwCiAgICAgICAgICAgIAogICAgICAgICAgICBzZWxmLl90YWJfY29udGFpbmVyID0gYyA9IGJhLmNvbnRhaW5lcndpZGdldChwYXJlbnQ9c2VsZi5fc2Nyb2xsd2lkZ2V0LAogICAgICAgICAgICAgICAgc2l6ZT0oc2VsZi5fc3ViX3dpZHRoLHN1Yl9oZWlnaHQpLAogICAgICAgICAgICAgICAgYmFja2dyb3VuZD1GYWxzZSxzZWxlY3Rpb25fbG9vcHNfdG9fcGFyZW50PVRydWUpCgogICAgICAgICAgICB0ID0gYmEudGV4dHdpZGdldChwYXJlbnQ9Yyxwb3NpdGlvbj0oMTEwLCB2LTIwKSxzaXplPSh3aWR0aCw1MCksCiAgICAgICAgICAgICAgICAgICAgICBzY2FsZT0xLjQsY29sb3I9KDAuMiwxLjIsMC4yKSxoX2FsaWduPSJjZW50ZXIiLHZfYWxpZ249ImNlbnRlciIsCiAgICAgICAgICAgICAgICAgICAgICB0ZXh0PSgiVWx0aW1hdGUgUG93ZXJ1cCBNYW5hZ2VyIHYxLjciKSxtYXh3aWR0aD13aWR0aCozMCkKCiAgICAgICAgICAgIHQgPSBiYS50ZXh0d2lkZ2V0KHBhcmVudD1jLHBvc2l0aW9uPSgxMTAsIHYtOTApLHNpemU9KHdpZHRoLDUwKSwKICAgICAgICAgICAgICAgICAgICAgIHNjYWxlPTEsY29sb3I9KDEuMywwLjUsMS4wKSxoX2FsaWduPSJjZW50ZXIiLHZfYWxpZ249ImNlbnRlciIsCiAgICAgICAgICAgICAgICAgICAgICB0ZXh0PWdldGxhbmd1YWdlKCdDcmVhdG9yJyksbWF4d2lkdGg9d2lkdGgqMzApCgogICAgICAgICAgICB0ID0gYmEudGV4dHdpZGdldChwYXJlbnQ9Yyxwb3NpdGlvbj0oMTEwLCB2LTIyMCksc2l6ZT0od2lkdGgsNTApLAogICAgICAgICAgICAgICAgICAgICAgc2NhbGU9MSxjb2xvcj0oMS4wLDEuMiwwLjMpLGhfYWxpZ249ImNlbnRlciIsdl9hbGlnbj0iY2VudGVyIiwKICAgICAgICAgICAgICAgICAgICAgIHRleHQ9Z2V0bGFuZ3VhZ2UoJ01vZCBJbmZvJyksbWF4d2lkdGg9d2lkdGgqMzApCiAgICAKICAgICAgICBmb3Igc2VsZWN0X3RhYixidXR0b25fdGFiIGluIHNlbGYudGFiX2J1dHRvbnMuaXRlbXMoKToKICAgICAgICAgICAgaWYgc2VsZWN0X3RhYiA9PSB0YWI6CiAgICAgICAgICAgICAgICBiYS5idXR0b253aWRnZXQoZWRpdD1idXR0b25fdGFiLGNvbG9yPSgwLjUsMC40LDEuNSkpCiAgICAgICAgICAgIGVsc2U6IGJhLmJ1dHRvbndpZGdldChlZGl0PWJ1dHRvbl90YWIsY29sb3I9KDAuNTIsMC40OCwwLjYzKSkKCiAgICBkZWYgX2FsbF9wb3B1cChzZWxmLCB0YWc6IHN0ciwgcG9wdXA6IHN0cikgLT4gTm9uZToKICAgICAgICBjb25maWdbdGFnXSA9IHBvcHVwCiAgICAgICAgYXBnLmFwcGx5X2FuZF9jb21taXQoKQoKICAgIGRlZiBfc2V0X2NvbmNlcHQoc2VsZiwgY29uY2VwdDogc3RyKSAtPiBOb25lOgogICAgICAgIEdMT0JBTFsnQ2xzIFBvd2VydXAnXSA9IGNvbmNlcHQKCiAgICAgICAgaWYgY29uY2VwdCA9PSAnUmVzZXQnOgogICAgICAgICAgICBmb3IgcG93ZXIsIGRlZmx0IGluIGRlZmF1bHRfcG93ZXJ1cHMoKS5pdGVtcygpOgogICAgICAgICAgICAgICAgcG93ZXJ1cHNbcG93ZXJdID0gZGVmbHQKICAgICAgICBlbGlmIGNvbmNlcHQgPT0gJ05vdGhpbmcnOgogICAgICAgICAgICBmb3IgcG93ZXIgaW4gZGVmYXVsdF9wb3dlcnVwcygpOgogICAgICAgICAgICAgICAgcG93ZXJ1cHNbcG93ZXJdID0gMAogICAgICAgIGVsaWYgY29uY2VwdCA9PSAnT25seSBCb21icyc6CiAgICAgICAgICAgIGZvciBwb3dlciwgZGVmbHQgaW4gZGVmYXVsdF9wb3dlcnVwcygpLml0ZW1zKCk6CiAgICAgICAgICAgICAgICBpZiAnQm9tYnMnIG5vdCBpbiBwb3dlcjoKICAgICAgICAgICAgICAgICAgICBwb3dlcnVwc1twb3dlcl0gPSAwCiAgICAgICAgICAgICAgICBlbHNlOiBwb3dlcnVwc1twb3dlcl0gPSAzCiAgICAgICAgZWxpZiBjb25jZXB0ID09ICdPbmx5IEl0ZW1zJzoKICAgICAgICAgICAgZm9yIHBvd2VyLCBkZWZsdCBpbiBkZWZhdWx0X3Bvd2VydXBzKCkuaXRlbXMoKToKICAgICAgICAgICAgICAgIGlmICdCb21icycgaW4gcG93ZXI6CiAgICAgICAgICAgICAgICAgICAgcG93ZXJ1cHNbcG93ZXJdID0gMAogICAgICAgICAgICAgICAgZWxzZTogcG93ZXJ1cHNbcG93ZXJdID0gZGVmbHQKICAgICAgICBlbGlmIGNvbmNlcHQgPT0gJ05ldyc6CiAgICAgICAgICAgIGRlZmF1bHRfcG93ZXIgPSBkZWZhdWx0X3Bvd2VydXBzKCkKICAgICAgICAgICAgbmV3X3Bvd2VydXBzID0gbGlzdChkZWZhdWx0X3Bvd2VyKVs5Ol0KICAgICAgICAgICAgZm9yIHBvd2VyLCBkZWZsdCBpbiBkZWZhdWx0X3Bvd2VyLml0ZW1zKCk6CiAgICAgICAgICAgICAgICBpZiBwb3dlciBub3QgaW4gbmV3X3Bvd2VydXBzOgogICAgICAgICAgICAgICAgICAgIHBvd2VydXBzW3Bvd2VyXSA9IDAKICAgICAgICAgICAgICAgIGVsc2U6IHBvd2VydXBzW3Bvd2VyXSA9IGRlZmx0CgogICAgICAgIGlmIG5vdCBTVE9SRVsnQnV5IEZpcmVib21icyddOgogICAgICAgICAgICBwb3dlcnVwc1snRmlyZSBCb21icyddID0gMAogICAgICAgICAgICAKICAgICAgICBzZWxmLl9zZXRfdGFiKCdBY3Rpb24gMScpCgogICAgZGVmIHRhbmtfc2hpZWxkX3BlcmNlbnRhZ2Uoc2VsZiwgdGFnKToKICAgICAgICBtYXggPSA5NgogICAgICAgIG1pbiA9IDQwCiAgICAgICAgaWYgdGFnID09ICdJbmNyZW1lbnQnOgogICAgICAgICAgICBjb25maWdbJ1RhbmsgU2hpZWxkIFBURyddICs9IDEKICAgICAgICAgICAgaWYgY29uZmlnWydUYW5rIFNoaWVsZCBQVEcnXSA+IG1heDoKICAgICAgICAgICAgICAgIGNvbmZpZ1snVGFuayBTaGllbGQgUFRHJ10gPSBtaW4KICAgICAgICBlbGlmIHRhZyA9PSAnRGVjcmVtZW50JzoKICAgICAgICAgICAgY29uZmlnWydUYW5rIFNoaWVsZCBQVEcnXSAtPSAxCiAgICAgICAgICAgIGlmIGNvbmZpZ1snVGFuayBTaGllbGQgUFRHJ10gPCBtaW46CiAgICAgICAgICAgICAgICBjb25maWdbJ1RhbmsgU2hpZWxkIFBURyddID0gbWF4CiAgICAgICAgICAgICAgICAKICAgICAgICBwb3JjZW50YWplID0gY29uZmlnWydUYW5rIFNoaWVsZCBQVEcnXQogICAgICAgIGlmIHBvcmNlbnRhamUgPiA1OTogY29sb3IgPSAoMCwxLDApCiAgICAgICAgZWxpZiBwb3JjZW50YWplIDwgNDA6IGNvbG9yID0gKDEsMSwwKQogICAgICAgIGVsc2U6IGNvbG9yID0gKDAsMSwwLjgpCiAgICAgICAgYmEudGV4dHdpZGdldChlZGl0PXNlbGYudGFua190ZXh0LAogICAgICAgICAgICB0ZXh0PXN0cihwb3JjZW50YWplKSsnJScsY29sb3I9Y29sb3IpCgogICAgZGVmIGhlYWx0aF9kYW1hZ2VfcGVyY2VudGFnZShzZWxmLCB0YWcpOgogICAgICAgIG1heCA9IDgwCiAgICAgICAgbWluID0gMzUKICAgICAgICBpZiB0YWcgPT0gJ0luY3JlbWVudCc6CiAgICAgICAgICAgIGNvbmZpZ1snSGVhbGluZyBEYW1hZ2UgUFRHJ10gKz0gMQogICAgICAgICAgICBpZiBjb25maWdbJ0hlYWxpbmcgRGFtYWdlIFBURyddID4gbWF4OgogICAgICAgICAgICAgICAgY29uZmlnWydIZWFsaW5nIERhbWFnZSBQVEcnXSA9IG1pbgogICAgICAgIGVsaWYgdGFnID09ICdEZWNyZW1lbnQnOgogICAgICAgICAgICBjb25maWdbJ0hlYWxpbmcgRGFtYWdlIFBURyddIC09IDEKICAgICAgICAgICAgaWYgY29uZmlnWydIZWFsaW5nIERhbWFnZSBQVEcnXSA8IG1pbjoKICAgICAgICAgICAgICAgIGNvbmZpZ1snSGVhbGluZyBEYW1hZ2UgUFRHJ10gPSBtYXgKICAgICAgICAgICAgICAgIAogICAgICAgIHBvcmNlbnRhamUgPSBjb25maWdbJ0hlYWxpbmcgRGFtYWdlIFBURyddCiAgICAgICAgaWYgcG9yY2VudGFqZSA+IDU5OiBjb2xvciA9ICgwLDEsMCkKICAgICAgICBlbGlmIHBvcmNlbnRhamUgPCA0MDogY29sb3IgPSAoMSwxLDApCiAgICAgICAgZWxzZTogY29sb3IgPSAoMCwxLDAuOCkKICAgICAgICBiYS50ZXh0d2lkZ2V0KGVkaXQ9c2VsZi5obGdfdGV4dCwKICAgICAgICAgICAgdGV4dD1zdHIocG9yY2VudGFqZSkrJyUnLGNvbG9yPWNvbG9yKQoKICAgIGRlZiBhcHBlcmFuY2VfcG93ZXJ1cHMoc2VsZiwgcG93ZXJ1cDogc3RyLCBJRDogc3RyKToKICAgICAgICBtYXggPSA3CiAgICAgICAgaWYgSUQgPT0gIi0iOgogICAgICAgICAgICBpZiBwb3dlcnVwc1twb3dlcnVwXSA9PSAwOgogICAgICAgICAgICAgICAgcG93ZXJ1cHNbcG93ZXJ1cF0gPSBtYXgKICAgICAgICAgICAgZWxzZTogcG93ZXJ1cHNbcG93ZXJ1cF0gLT0gMQogICAgICAgIGVsaWYgSUQgPT0gIisiOgogICAgICAgICAgICBpZiBwb3dlcnVwc1twb3dlcnVwXSA9PSBtYXg6CiAgICAgICAgICAgICAgICBwb3dlcnVwc1twb3dlcnVwXSA9IDAKICAgICAgICAgICAgZWxzZTogcG93ZXJ1cHNbcG93ZXJ1cF0gKz0gMQogICAgICAgIGVudW0gPSBwb3dlcnVwc1twb3dlcnVwXQogICAgICAgIGJhLnRleHR3aWRnZXQoZWRpdD1zZWxmLmxpc3Rwb3dlcltwb3dlcnVwXSwKICAgICAgICAgICAgICAgICAgICAgIHRleHQ9c3RyKHBvd2VydXBzW3Bvd2VydXBdKSwKICAgICAgICAgICAgICAgICAgICAgIGNvbG9yPWNsc19wb3dfY29sb3IoKVtlbnVtXSkKICAgICAgICAgICAKICAgIGRlZiBfcG93ZXJ1cHNfc2NhbGUoc2VsZiwgSUQ6IHN0cik6CiAgICAgICAgbWF4ID0gMS41CiAgICAgICAgbWluID0gMC41CiAgICAgICAgc2MgPSAwLjEKICAgICAgICBpZiBJRCA9PSAiLSI6CiAgICAgICAgICAgIGlmIGNvbmZpZ1snUG93ZXJ1cCBTY2FsZSddIDwgKG1pbiswLjEpOgogICAgICAgICAgICAgICAgY29uZmlnWydQb3dlcnVwIFNjYWxlJ10gPSBtYXgKICAgICAgICAgICAgZWxzZTogY29uZmlnWydQb3dlcnVwIFNjYWxlJ10gLT0gc2MKICAgICAgICBlbGlmIElEID09ICIrIjoKICAgICAgICAgICAgaWYgY29uZmlnWydQb3dlcnVwIFNjYWxlJ10gPiAobWF4LTAuMSk6CiAgICAgICAgICAgICAgICBjb25maWdbJ1Bvd2VydXAgU2NhbGUnXSA9IG1pbgogICAgICAgICAgICBlbHNlOiBjb25maWdbJ1Bvd2VydXAgU2NhbGUnXSArPSBzYwogICAgICAgIGNvbmZpZ1snUG93ZXJ1cCBTY2FsZSddID0gcm91bmQoY29uZmlnWydQb3dlcnVwIFNjYWxlJ10sMSkKICAgICAgICBiYS50ZXh0d2lkZ2V0KGVkaXQ9c2VsZi50eHRfc2NhbGUsCiAgICAgICAgICAgICAgICAgICAgICB0ZXh0PXN0cihjb25maWdbJ1Bvd2VydXAgU2NhbGUnXSkpCiAgICAgICAgICAgCiAgICBkZWYgdG90YWxfcGVyY2VudGFnZShzZWxmLCBwb3dlcik6CiAgICAgICAgdG90YWwgPSAwCiAgICAgICAgcHcgPSBwb3dlcnVwc1twb3dlcl0KICAgICAgICBmb3IgaSxpMiBpbiBwb3dlcnVwcy5pdGVtcygpOgogICAgICAgICAgICB0b3RhbCArPSBpMgogICAgICAgIGlmIHRvdGFsID09IDA6CiAgICAgICAgICAgIHJldHVybiBmbG9hdCh0b3RhbCkKICAgICAgICBlbHNlOgogICAgICAgICAgICBwdGcgPSAoMTAwKnB3L3RvdGFsKQogICAgICAgICAgICByZXN1bHQgPSByb3VuZChwdGcsMikKICAgICAgICAgICAgcmV0dXJuIHJlc3VsdAogICAgICAgICAgIAogICAgZGVmIHN0b3JlX3JlZnJlc2goc2VsZiwgdGFnOiBzdHIpOgogICAgICAgIGlmIHRhZyA9PSAnQnV5IEZpcmVib21icyc6CiAgICAgICAgICAgIHBvd2VydXBzWydGaXJlIEJvbWJzJ10gPSAzCiAgICAgICAgICAgIHNlbGYuZGVmYXVsdF9wb3dlcl9saXN0LmFwcGVuZCgnRmlyZSBCb21icycpCiAgICAgICAgc2VsZi5fc2V0X3RhYignQWN0aW9uIDQnKQogICAgICAgICAgIAogICAgZGVmIF9idXlfb2JqZWN0KHNlbGYsIHRhZzogc3RyLCBwcmljZTogaW50KToKICAgICAgICBzdG9yZSA9IEJlYXJTdG9yZSh2YWx1ZT10YWcsIHByaWNlPXByaWNlLAogICAgICAgICAgICAgICAgY2FsbGJhY2s9YmEuQ2FsbChzZWxmLnN0b3JlX3JlZnJlc2gsdGFnKSkKICAgICAgICBzdG9yZS5idXkoKQogICAgICAgICAgIAogICAgZGVmIF9wcm9tb2NvZGUoc2VsZik6CiAgICAgICAgY29kZSA9IGJhLnRleHR3aWRnZXQocXVlcnk9c2VsZi5wcm9tb2NvZGVfdGV4dCkKICAgICAgICBwcm9tbyA9IFByb21vQ29kZShjb2RlPWNvZGUpCiAgICAgICAgcHJvbW8uY29kZV9jb25maXJtYXRpb24oKQogICAgICAgIGJhLnRleHR3aWRnZXQoZWRpdD1zZWxmLnByb21vY29kZV90ZXh0LHRleHQ9IiIpCiAgICAgICAgICAgCiAgICBkZWYgX3N3aXRjaGVzKHNlbGYsdGFnLG0pOgogICAgICAgIGNvbmZpZ1t0YWddID0gRmFsc2UgaWYgbT09MCBlbHNlIFRydWUKICAgICAgICBhcGcuYXBwbHlfYW5kX2NvbW1pdCgpCiAgICAgICAgICAgCiAgICBkZWYgX3BlcmNlbnRhZ2Vfd2luZG93KHNlbGYpOgogICAgICAgIHNlbGYuX3NldF90YWIoJ1BlcmNlbnRhZ2UnKQogICAgICAgICAgIAogICAgZGVmIF9iYWNrKHNlbGYpOgogICAgICAgIGJhLmNvbnRhaW5lcndpZGdldChlZGl0PXNlbGYuX3Jvb3Rfd2lkZ2V0LHRyYW5zaXRpb249J291dF9sZWZ0JykKICAgICAgICBicm93c2VyLlByb2ZpbGVCcm93c2VyV2luZG93KCkK").decode("utf-8")) +import ba,_ba,random,time,datetime,weakref,json +import ba.internal +from bastd.ui.profile import browser +from bastd.actor import bomb +from bastd.actor import powerupbox as pupbox +from bastd.actor.spazbot import SpazBot +from bastd.actor.bomb import (Bomb,Blast) +from bastd.ui.popup import (PopupWindow,PopupMenuWindow,PopupMenu) +from bastd.actor.spaz import (Spaz,SpazFactory,PickupMessage, PunchHitMessage, + CurseExplodeMessage, BombDiedMessage) +from bastd.mainmenu import (MainMenuActivity,MainMenuSession) +from bastd.gameutils import SharedObjects +from bastd.actor.powerupbox import PowerupBoxFactory +from bastd.actor.popuptext import PopupText +from bastd.ui.confirm import ConfirmWindow +from bastd.actor.spaz import * + +if TYPE_CHECKING: + pass + +# === Mod made by @Patron_Modz === + +def getlanguage(text, subs: str = None, almacen: list = []): + if almacen == []: almacen = list(range(1000)) + lang = _ba.app.lang.language + translate = {"Reset": + {"Spanish": "Reiniciar", + "English": "Reset", + "Portuguese": "Reiniciar"}, + "Nothing": + {"Spanish": "Sin potenciadores", + "English": "No powerups", + "Portuguese": "Sem powerups"}, + "Action 1": + {"Spanish": "Potenciadores", + "English": "Powerups", + "Portuguese": "Powerups"}, + "Action 2": + {"Spanish": "Configuración", + "English": "Settings", + "Portuguese": "Definições"}, + "Action 3": + {"Spanish": "Extras", + "English": "Extras", + "Portuguese": "Extras"}, + "Action 4": + {"Spanish": "Tienda", + "English": "Store", + "Portuguese": "Loja"}, + "Action 5": + {"Spanish": "Canjear código", + "English": "Enter Code", + "Portuguese": "Código promocional"}, + "Custom": + {"Spanish": "", + "English": "Customize", + "Portuguese": "Customizar"}, + "Impairment Bombs": + {"Spanish": "Bombas menoscabo", + "English": "Hyperactive bombs", + "Portuguese": "Bombas hiperativas"}, + "Speed": + {"Spanish": "Velocidad", + "English": "Speed", + "Portuguese": "Velocidade"}, + "Fire Bombs": + {"Spanish": "Bombas de fuego", + "English": "Fire Bombs", + "Portuguese": "Bombas de fogo"}, + "Ice Man": + {"Spanish": "Hombre de hielo", + "English": "Ice man", + "Portuguese": "Homem de gelo"}, + "Fly Bombs": + {"Spanish": "Bombas expansivas", + "English": "Expansive bombs", + "Portuguese": "Bombas expansivas"}, + "Goodbye": + {"Spanish": "¡Hasta luego!", + "English": "Goodbye!", + "Portuguese": "Adeus!"}, + "Healing Damage": + {"Spanish": "Auto-curación", + "English": "Healing Damage", + "Portuguese": "Auto-cura"}, + "Tank Shield": + {"Spanish": "Súper blindaje", + "English": "Reinforced shield", + "Portuguese": "Escudo reforçado"}, + "Tank Shield PTG": + {"Spanish": "Porcentaje de disminución", + "English": "Percentage decreased", + "Portuguese": "Percentual reduzido"}, + "Healing Damage PTG": + {"Spanish": "Porcentaje de recuperación de salud", + "English": "Percentage of health recovered", + "Portuguese": "Porcentagem de recuperação de saúde"}, + "SY: BALL": + {"Spanish": "Esfera", + "English": "Sphere", + "Portuguese": "Esfera"}, + "SY: Impact": + {"Spanish": "Especial", + "English": "Special", + "Portuguese": "Especial"}, + "SY: Egg": + {"Spanish": "Huevito", + "English": "Egg shape", + "Portuguese": "Ovo"}, + "Powerup Scale": + {"Spanish": "Tamaño del potenciador", + "English": "Powerups size", + "Portuguese": "Tamanho de powerups"}, + "Powerup With Shield": + {"Spanish": "Potenciadores con escudo", + "English": "Powerups with shield", + "Portuguese": "Powerups com escudo"}, + "Powerup Time": + {"Spanish": "Mostrar Temporizador", + "English": "Show end time", + "Portuguese": "Mostrar cronômetro"}, + "Powerup Style": + {"Spanish": "Forma de los potenciadores", + "English": "Shape of powerup", + "Portuguese": "Forma de powerup"}, + "Powerup Name": + {"Spanish": "Mostrar nombre en los potenciadores", + "English": "Show name on powerups", + "Portuguese": "Mostrar nome em powerups"}, + "Percentage": + {"Spanish": "Probabilidad", + "English": "Show percentage", + "Portuguese": "Mostrar porcentagem"}, + "Only Items": + {"Spanish": "Sólo Accesorios", + "English": "Only utensils", + "Portuguese": "Apenas utensilios"}, + "New": + {"Spanish": "Nuevo", + "English": "New", + "Portuguese": "Novo"}, + "Only Bombs": + {"Spanish": "Sólo Bombas", + "English": "Only bombs", + "Portuguese": "Apenas bombas"}, + "Coins 0": + {"Spanish": "Monedas Insuficientes", + "English": "Insufficient coins", + "Portuguese": "Moedas insuficientes"}, + "Purchase": + {"Spanish": "Compra realizada correctamente", + "English": "Successful purchase", + "Portuguese": "Compra Bem Sucedida"}, + "Double Product": + {"Spanish": "Ya has comprado este artículo", + "English": "You've already bought this", + "Portuguese": "Voce ja comprou isto"}, + "Bought": + {"Spanish": "Comprado", + "English": "Bought", + "Portuguese": "Comprou"}, + "Confirm Purchase": + {"Spanish": f'Tienes {subs} monedas. {_sp_} ¿Deseas comprar esto?', + "English": f'You have {subs} coins. {_sp_} Do you want to buy this?', + "Portuguese": f'Você tem {subs} moedas. {_sp_} Deseja comprar isto?'}, + "FireBombs Store": + {"Spanish": 'Bombas de fuego', + "English": 'Fire bombs', + "Portuguese": 'Bombas de incêndio'}, + "Timer Store": + {"Spanish": 'Temporizador', + "English": 'Timer', + "Portuguese": 'Timer'}, + "Percentages Store": + {"Spanish": 'Extras', + "English": 'Extras', + "Portuguese": 'Extras'}, + "Block Option Store": + {"Spanish": f"Uuups..{_sp_}Esta opción está bloqueada.{_sp_} Para acceder a ella puedes {_sp_} comprarla en la tienda.{_sp_} Gracias...", + "English": f"Oooops...{_sp_}This option is blocked. {_sp_} To access it you can buy {_sp_} it in the store.{_sp_} Thank you...", + "Portuguese": f"Ooops...{_sp_}Esta opção está bloqueada. {_sp_} Para acessá-lo, você pode {_sp_} comprá-lo na loja.{_sp_} Obrigado..."}, + "True Code": + {"Spanish": "¡Código canjeado!", + "English": "Successful code!", + "Portuguese": "¡Código válido!"}, + "False Code": + {"Spanish": "Código ya canjeado", + "English": "Expired code", + "Portuguese": "Código expirado"}, + "Invalid Code": + {"Spanish": "Código inválido", + "English": "Invalid code", + "Portuguese": "Código inválido"}, + "Reward Code": + {"Spanish": f"¡Felicitaciones! ¡Ganaste {subs} monedas!", + "English": f"Congratulations! You've {subs} coins", + "Portuguese": f"Parabéns! Você tem {subs} moedas"}, + "Creator": + {"Spanish": "Mod creado por @PatrónModz", + "English": "Mod created by @PatrónModz", + "Portuguese": "Mod creado by @PatrónModz"}, + "Mod Info": + {"Spanish": f"Un mod genial que te permite gestionar {_sp_} los potenciadores a tu antojo. {_sp_} también incluye 8 potenciadores extra{_sp_} dejando 17 en total... ¡Guay!", + "English": f"A cool mod that allows you to manage {_sp_} powerups at your whims. {_sp_} also includes 8 extra powerups{_sp_} leaving 17 in total... Wow!", + "Portuguese": f"Um mod legal que permite que você gerencie os{_sp_} powerups de de acordo com seus caprichos. {_sp_} também inclui 8 powerups extras,{_sp_} deixando 17 no total... Uau!"}, + "Coins Message": + {"Spanish": f"Recompensa: {subs} Monedas", + "English": f"Reward: {subs} Coins", + "Portuguese": f"Recompensa: {subs} Moedas"}, + "Coins Limit Message": + {"Spanish": f"Ganaste {almacen[0]} Monedas.{_sp_} Pero has superado el límite de {almacen[1]}", + "English": f"You won {almacen[0]} Coins. {_sp_} But you have exceeded the limit of {almacen[1]}", + "Portuguese": f"Você ganhou {almacen[0]} Moedas. {_sp_} Mas você excedeu o limite de {almacen[1]}"}, + } + languages = ['Spanish','Portuguese','English'] + if lang not in languages: lang = 'English' + + if text not in translate: + return text + + return translate[text][lang] + +import setting +settings=setting.get_settings_data() + +def settings_distribution(): + return settings["elPatronPowerups"]["settings"] + + + +apg = ba.app.config + +apg['PPM Settings'] = settings_distribution() + + +config = apg['PPM Settings'] + +def default_powerups(): + return settings["elPatronPowerups"]["Quantity"] + + +config['Powerups'] = default_powerups() + + +powerups = config['Powerups'] + +# === EXTRAS === + +GLOBAL = {"Tab": 'Action 1', + "Cls Powerup": 0, + "Coins Message": []} + +# === STORE === +def promo_codes(): + return {"G-Am54igO42Os": [True,1100], + "P-tRo8nM8dZ": [True,2800], + "Y-tU2B3S": [True,500], + "B-0mB3RYT2z": [True,910], + "B-Asd14mON9G0D": [True,910], + "D-rAcK0cJ23": [True,910], + "E-a27ZO6f3Y": [True,600], + "E-Am54igO42Os": [True,600], + "E-M4uN3K34XB": [True,840], + "PM-731ClcAF": [True,50000]} + +def store_items(): + return {"Buy Firebombs": False, + "Buy Option": False, + "Buy Percentage": False} + +if apg.get('Bear Coin') is None: + apg['Bear Coin'] = 0 + apg.apply_and_commit() + +if apg.get('Bear Coin') is not None: + if apg['Bear Coin'] <= 0: + apg['Bear Coin'] = 0 + apg['Bear Coin'] = int(apg['Bear Coin']) + +if apg.get('Bear Store') is None: + apg['Bear Store'] = {} + +for i,j in store_items().items(): + store = apg['Bear Store'] + if i not in store: + if store.get(i) is None: + store[i] = j + apg.apply_and_commit() + +STORE = apg['Bear Store'] + +if STORE.get('Promo Code') is None: + STORE['Promo Code'] = promo_codes() + +for i,x in promo_codes().items(): + pmcode = STORE['Promo Code'] + if i not in pmcode: + if pmcode.get(i) is None: + pmcode[i] = x + +apg.apply_and_commit() + +class BearStore: + def __init__(self, + price: int = 1000, + value: str = '', + callback: call = None): + + self.price = price + self.value = value + self.store = STORE[value] + self.coins = apg['Bear Coin'] + self.callback = callback + + def buy(self): + if not self.store: + if self.coins >= (self.price): + def confirm(): + STORE[self.value] = True + apg['Bear Coin'] -= int(self.price) + ba.screenmessage(getlanguage('Purchase'),(0,1,0)) + ba.playsound(ba.getsound('cashRegister')) + apg.apply_and_commit() + self.callback() + ConfirmWindow(getlanguage('Confirm Purchase',subs=self.coins), + width=400, height=120, action=confirm, ok_text=ba.Lstr(resource='okText')) + else: + ba.screenmessage(getlanguage('Coins 0'),(1,0,0)) + ba.playsound(ba.getsound('error')) + else: + ba.screenmessage(getlanguage('Double Product'),(1,0,0)) + ba.playsound(ba.getsound('error')) + + def __del__(self): + apg['Bear Coin'] = int(apg['Bear Coin']) + apg.apply_and_commit() + +class PromoCode: + def __init__(self, code: str = ''): + self.code = code + self.codes_store = STORE['Promo Code'] + if self.code in self.codes_store: + self.code_type = STORE['Promo Code'][code] + self.promo_code_expire = self.code_type[0] + self.promo_code_amount = self.code_type[1] + + def __del__(self): + apg['Bear Coin'] = int(apg['Bear Coin']) + apg.apply_and_commit() + + def code_confirmation(self): + if self.code != "": + ba.screenmessage( + ba.Lstr(resource='submittingPromoCodeText'),(0,1,0)) + ba.timer(2,ba.Call(self.validate_code)) + + def validate_code(self): + if self.code in self.codes_store: + if self.promo_code_expire: + ba.timer(1.5,ba.Call(self.successful_code)) + ba.screenmessage(getlanguage('True Code'),(0,1,0)) + ba.playsound(ba.getsound('cheer')) + self.code_type[0] = False + else: + ba.screenmessage(getlanguage('False Code'),(1,0,0)) + ba.playsound(ba.getsound('error')) + else: + ba.screenmessage(getlanguage('Invalid Code'),(1,0,0)) + ba.playsound(ba.getsound('error')) + + def successful_code(self): + apg['Bear Coin'] += self.promo_code_amount + ba.screenmessage(getlanguage('Reward Code', + subs=self.promo_code_amount),(0,1,0)) + ba.playsound(ba.getsound('cashRegister2')) + +MainMenuActivity.super_transition_in = MainMenuActivity.on_transition_in +def new_on_transition_in(self): + self.super_transition_in() + limit = 8400 + bear_coin = apg['Bear Coin'] + coins_message = GLOBAL['Coins Message'] + try: + if not (STORE['Buy Firebombs'] and + STORE['Buy Option'] and + STORE['Buy Percentage']): + + if coins_message != []: + result = 0 + for i in coins_message: + result += i + + if not bear_coin >= (limit-1): + ba.screenmessage(getlanguage('Coins Message',subs=result),(0,1,0)) + ba.playsound(ba.getsound('cashRegister')) + else: + ba.screenmessage(getlanguage('Coins Limit Message', + almacen=[result,limit]),(1,0,0)) + ba.playsound(ba.getsound('error')) + self.bear_coin_message = True + GLOBAL['Coins Message'] = [] + except: pass + +SpazBot.super_handlemessage = SpazBot.handlemessage +def bot_handlemessage(self, msg: Any): + self.super_handlemessage(msg) + if isinstance(msg, ba.DieMessage): + if not self.die: + self.die = True + self.limit = 8400 + self.free_coins = random.randint(1,25) + self.bear_coins = apg['Bear Coin'] + + if not self.bear_coins >= (self.limit): + self.bear_coins += self.free_coins + GLOBAL['Coins Message'].append(self.free_coins) + + if self.bear_coins >= (self.limit): + self.bear_coins = self.limit + + apg['Bear Coin'] = int(self.bear_coins) + apg.apply_and_commit() + + else: GLOBAL['Coins Message'].append(self.free_coins) + +def cls_pow_color(): + return [(1,0.1,0.1),(0.1,0.5,0.9),(0.1,0.9,0.9), + (0.1,0.9,0.1),(0.1,1,0.5),(1,1,0.2),(2,0.5,0.5),(1,0,6)] + +def random_color(): + a = random.random()*3 + b = random.random()*3 + c = random.random()*3 + return (a,b,c) + +def powerup_dist(): + return (('triple_bombs', powerups['Triple']), + ('ice_bombs', powerups['Ice Bombs']), + ('punch', powerups['Punch']), + ('impact_bombs', powerups['Impact Bombs']), + ('land_mines', powerups['Mine Bombs']), + ('sticky_bombs', powerups['Sticky Bombs']), + ('shield', powerups['Shield']), + ('health', powerups['Health']), + ('curse', powerups['Curse']), + ('speed',powerups['Speed']), + ('health_damage', powerups['Healing Damage']), + ('goodbye',powerups['Goodbye']), + ('ice_man',powerups['Ice Man']), + ('tank_shield',powerups['Tank Shield']), + ('impairment_bombs',powerups['Impairment Bombs']), + ('fire_bombs',powerups['Fire Bombs']), + ('fly_bombs',powerups['Fly Bombs'])) + +def percentage_tank_shield(): + percentage = config['Tank Shield PTG'] + percentage_text = ('0.') + str(percentage) + return float(percentage_text) + +def percentage_health_damage(): + percentage = config['Healing Damage PTG'] + percentage_text = ('0.') + str(percentage) + return float(percentage_text) + +# === Modify class === + +class NewProfileBrowserWindow(browser.ProfileBrowserWindow): + def __init__(self, + transition: str = 'in_right', + in_main_menu: bool = True, + selected_profile: str = None, + origin_widget: ba.Widget = None): + super().__init__(transition,in_main_menu,selected_profile,origin_widget) + + self.session = ba.internal.get_foreground_host_session() + uiscale = ba.app.ui.uiscale + width = (100 if uiscale is + ba.UIScale.SMALL else -14) + size = 50 + position = (width*1.65,300) + + if isinstance(self.session,MainMenuSession): + self.button = ba.buttonwidget(parent=self._root_widget, + autoselect=True,position=position, + size=(size,size),button_type='square', + label='',on_activate_call=ba.Call(self.powerupmanager_window)) + + size = size*0.60 + self.image = ba.imagewidget(parent=self._root_widget, + size=(size,size),draw_controller=self.button, + position=(position[0]+10.5,position[1]+17), + texture=ba.gettexture('powerupSpeed')) + + self.text = ba.textwidget(parent=self._root_widget, + position=(position[0]+25,position[1]+10), + size=(0, 0),scale=0.45,color=(0.7,0.9,0.7,1.0), + draw_controller=self.button,maxwidth=60, + text=(f"Ultimate Powerup {_sp_}Manager"),h_align='center',v_align='center') + + def powerupmanager_window(self): + ba.containerwidget(edit=self._root_widget,transition='out_left') + PowerupManagerWindow() + +class NewPowerupBoxFactory(pupbox.PowerupBoxFactory): + def __init__(self) -> None: + super().__init__() + self.tex_speed = ba.gettexture('powerupSpeed') + self.tex_health_damage = ba.gettexture('heart') + self.tex_goodbye = ba.gettexture('achievementOnslaught') + self.tex_ice_man = ba.gettexture('ouyaUButton') + self.tex_tank_shield = ba.gettexture('achievementSuperPunch') + self.tex_impairment_bombs = ba.gettexture('levelIcon') + self.tex_fire_bombs = ba.gettexture('ouyaOButton') + self.tex_fly_bombs = ba.gettexture('star') + + self._powerupdist = [] + for powerup, freq in powerup_dist(): + for _i in range(int(freq)): + self._powerupdist.append(powerup) + + def get_random_powerup_type(self,forcetype = None, excludetypes = None): + + try: self.mapa = ba.getactivity()._map.getname() + except: self.mapa = None + + speed_banned_maps = ['Hockey Stadium','Lake Frigid','Happy Thoughts'] + + if self.mapa in speed_banned_maps: + powerup_disable = ['speed'] + else: powerup_disable = [] + + if excludetypes is None: + excludetypes = [] + if forcetype: + ptype = forcetype + else: + if self._lastpoweruptype == 'curse': + ptype = 'health' + else: + while True: + ptype = self._powerupdist[random.randint( + 0, + len(self._powerupdist) - 1)] + if ptype not in excludetypes and ptype not in powerup_disable: break + self._lastpoweruptype = ptype + return ptype + +def fire_effect(self): + if self.node.exists(): + ba.emitfx(position=self.node.position, + scale=3,count=50*2,spread=0.3, + chunk_type='sweat') + else: self.fire_effect_time = None + +###########BOMBS +Bomb._pm_old_bomb = Bomb.__init__ +def _bomb_init(self, + position: Sequence[float] = (0.0, 1.0, 0.0), + velocity: Sequence[float] = (0.0, 0.0, 0.0), + bomb_type: str = 'normal', + blast_radius: float = 2.0, + bomb_scale: float = 1.0, + source_player: ba.Player = None, + owner: ba.Node = None): + + self.bm_type = bomb_type + new_bomb_type = bomb_type + bombs = ['ice_bubble','impairment','fire','fly'] + + if bomb_type in bombs: + new_bomb_type = 'ice' + + self._pm_old_bomb(position,velocity,new_bomb_type,blast_radius, + bomb_scale,source_player,owner) + + tex = self.node.color_texture + + if self.bm_type == 'ice_bubble': + self.bomb_type = self.bm_type + self.node.model = None + self.shield_ice = ba.newnode('shield',owner=self.node, + attrs={'color': (0.5, 1.0, 7.0),'radius': 0.6}) + self.node.connectattr('position', self.shield_ice, 'position') + elif self.bm_type == 'fire': + self.bomb_type = self.bm_type + self.node.model = None + self.shield_fire = ba.newnode('shield',owner=self.node, + attrs={'color': (6.5, 6.5, 2.0),'radius': 0.6}) + self.node.connectattr('position', self.shield_fire, 'position') + self.fire_effect_time = ba.Timer(0.1,ba.Call(fire_effect,self),repeat=True) + elif self.bm_type == 'impairment': + self.bomb_type = self.bm_type + tex = ba.gettexture('eggTex3') + elif self.bm_type == 'fly': + self.bomb_type = self.bm_type + tex = ba.gettexture('eggTex1') + + self.node.color_texture = tex + self.hit_subtype = self.bomb_type + + if self.bomb_type == 'ice_bubble': + self.blast_radius *= 1.2 + elif self.bomb_type == 'fly': + self.blast_radius *= 2.2 + +def bomb_handlemessage(self, msg: Any) -> Any: + assert not self.expired + + if isinstance(msg, ba.DieMessage): + if self.node: + self.node.delete() + + elif isinstance(msg, bomb.ExplodeHitMessage): + node = ba.getcollision().opposingnode + assert self.node + nodepos = self.node.position + mag = 2000.0 + if self.blast_type in ('ice','ice_bubble'): + mag *= 0.5 + elif self.blast_type == 'land_mine': + mag *= 2.5 + elif self.blast_type == 'tnt': + mag *= 2.0 + elif self.blast_type == 'fire': + mag *= 0.6 + elif self.blast_type == 'fly': + mag *= 5.5 + + node.handlemessage( + ba.HitMessage(pos=nodepos, + velocity=(0, 0, 0), + magnitude=mag, + hit_type=self.hit_type, + hit_subtype=self.hit_subtype, + radius=self.radius, + source_player=ba.existing(self._source_player))) + if self.blast_type in ('ice','ice_bubble'): + ba.playsound(bomb.BombFactory.get().freeze_sound, + 10, + position=nodepos) + node.handlemessage(ba.FreezeMessage()) + + return None + +def powerup_translated(self, type: str): + powerups_names = {'triple_bombs': ba.Lstr(resource='helpWindow.'+'powerupBombNameText'), + 'ice_bombs': ba.Lstr(resource='helpWindow.'+'powerupIceBombsNameText'), + 'punch': ba.Lstr(resource='helpWindow.'+'powerupPunchNameText'), + 'impact_bombs': ba.Lstr(resource='helpWindow.'+'powerupImpactBombsNameText'), + 'land_mines': ba.Lstr(resource='helpWindow.'+'powerupLandMinesNameText'), + 'sticky_bombs': ba.Lstr(resource='helpWindow.'+'powerupStickyBombsNameText'), + 'shield': ba.Lstr(resource='helpWindow.'+'powerupShieldNameText'), + 'health': ba.Lstr(resource='helpWindow.'+'powerupHealthNameText'), + 'curse': ba.Lstr(resource='helpWindow.'+'powerupCurseNameText'), + 'speed': getlanguage('Speed'), + 'health_damage': getlanguage('Healing Damage'), + 'goodbye': getlanguage('Goodbye'), + 'ice_man': getlanguage('Ice Man'), + 'tank_shield': getlanguage('Tank Shield'), + 'impairment_bombs': getlanguage('Impairment Bombs'), + 'fire_bombs': getlanguage('Fire Bombs'), + 'fly_bombs': getlanguage('Fly Bombs')} + self.texts['Name'].text = powerups_names[type] + +###########POWERUP +pupbox.PowerupBox._old_pbx_ = pupbox.PowerupBox.__init__ +def _pbx_(self, position: Sequence[float] = (0.0, 1.0, 0.0), + poweruptype: str = 'triple_bombs', + expire: bool = True): + + self.news: list = [] + for x,i in powerup_dist(): self.news.append(x) + + self.box: list = [] + self.texts = {} + self.news = self.news[9:] + self.box.append(poweruptype) + self.npowerup = self.box[0] + factory = NewPowerupBoxFactory.get() + + if self.npowerup in self.news: new_poweruptype = 'shield' + else: new_poweruptype = poweruptype + self._old_pbx_(position,new_poweruptype,expire) + + type = new_poweruptype + tex = self.node.color_texture + model = self.node.model + + if self.npowerup == 'speed': + type = self.npowerup + tex = factory.tex_speed + elif self.npowerup == 'health_damage': + type = self.npowerup + tex = factory.tex_health_damage + elif self.npowerup == 'goodbye': + type = self.npowerup + tex = factory.tex_goodbye + elif self.npowerup == 'ice_man': + type = self.npowerup + tex = factory.tex_ice_man + elif self.npowerup == 'tank_shield': + type = self.npowerup + tex = factory.tex_tank_shield + elif self.npowerup == 'impairment_bombs': + type = self.npowerup + tex = factory.tex_impairment_bombs + elif self.npowerup == 'fire_bombs': + type = self.npowerup + tex = factory.tex_fire_bombs + elif self.npowerup == 'fly_bombs': + type = self.npowerup + tex = factory.tex_fly_bombs + + self.poweruptype = type + self.node.model = model + self.node.color_texture = tex + n_scale = config['Powerup Scale'] + style = config['Powerup Style'] + + curve = ba.animate(self.node, 'model_scale', {0: 0, 0.14: 1.6, 0.2: n_scale}) + ba.timer(0.2, curve.delete) + + def util_text(type: str, text: str, scale: float = 1, color: list = [1,1,1], + position: list = [0, 0.7, 0], colors_name: bool = False): + m = ba.newnode('math',owner=self.node,attrs={'input1': + (position[0], position[1], position[2]),'operation': 'add'}) + self.node.connectattr('position', m, 'input2') + self.texts[type] = ba.newnode('text',owner=self.node, + attrs={'text': str(text), + 'in_world': True, + 'scale': 0.02, + 'shadow': 0.5, + 'flatness': 1.0, + 'color': (color[0],color[1],color[2]), + 'h_align': 'center'}) + m.connectattr('output', self.texts[type], 'position') + ba.animate(self.texts[type], 'scale', {0: 0.017,0.4: 0.017, 0.5: 0.01*scale}) + + if colors_name: + ba.animate_array(self.texts[type], 'color', 3, + {0:(1,0,0), + 0.2:(1,0.5,0), + 0.4:(1,1,0), + 0.6:(0,1,0), + 0.8:(0,1,1), + 1.0:(1,0,1), + 1.2:(1,0,0)},True) + + def update_time(time): + if self.texts['Time'].exists(): + self.texts['Time'].text = str(time) + + if config['Powerup Time']: + interval = int(pupbox.DEFAULT_POWERUP_INTERVAL) + time2 = (interval-1) + time = 1 + + util_text('Time', time2, scale=1.5,color=(2,2,2), + position=[0,0.9,0], colors_name=False) + + while(interval+3): + ba.timer(time-1,ba.Call(update_time,f'{time2}s')) + + if time2 == 0: + break + + time += 1 + time2 -= 1 + + if config['Powerup With Shield']: + scale = config['Powerup Scale'] + self.shield = ba.newnode('shield',owner=self.node,attrs={'color': (1,1,0),'radius': 1.3*scale}) + self.node.connectattr('position', self.shield, 'position') + ba.animate_array(self.shield,'color',3,{0: (2,0,0), 0.5: (0,2,0), 1: (0,1,6), 1.5: (2,0,0)},True) + + if config['Powerup Name']: + util_text('Name',self.poweruptype,scale=1.2, + position=[0,0.4,0],colors_name=True) + powerup_translated(self,self.poweruptype) + + if style == 'SY: BALL': + self.node.model = ba.getmodel('frostyPelvis') + elif style == 'SY: Impact': + self.node.model = ba.getmodel('impactBomb') + elif style == 'SY: Egg': + self.node.model = ba.getmodel('egg') + +###########SPAZ +def _speed_off_flash(self): + if self.node: + factory = NewPowerupBoxFactory.get() + self.node.billboard_texture = factory.tex_speed + self.node.billboard_opacity = 1.0 + self.node.billboard_cross_out = True + +def _speed_wear_off(self): + if self.node: + self.node.hockey = False + self.node.billboard_opacity = 0.0 + ba.playsound(ba.getsound('powerdown01')) + +def _ice_man_off_flash(self): + if self.node: + factory = NewPowerupBoxFactory.get() + self.node.billboard_texture = factory.tex_ice_man + self.node.billboard_opacity = 1.0 + self.node.billboard_cross_out = True + +def _ice_man_wear_off(self): + if self.node: + f = self.color[0] + i = (0,1,4) + + bomb = self.bmb_color[0] + if bomb != 'ice_bubble': self.bomb_type = bomb + else: self.bomb_type = 'normal' + + self.freeze_punch = False + self.node.billboard_opacity = 0.0 + ba.animate_array(self.node,'color',3,{0: f, 0.3: i, 0.6: f}) + ba.playsound(ba.getsound('powerdown01')) + +Spaz._pm2_spz_old = Spaz.__init__ +def _init_spaz_(self,*args, **kwargs): + self._pm2_spz_old(*args, **kwargs) + self.edg_eff = False + self.kill_eff = False + self.freeze_punch = False + self.die = False + self.color: list = [] + self.color.append(self.node.color) + + self.tankshield = {"Tank": False, + "Reduction": False, + "Shield": None} + +Spaz._super_on_punch_press = Spaz.on_punch_press +def spaz_on_punch_press(self) -> None: + self._super_on_punch_press() + + if self.tankshield['Tank']: + try: + self.tankshield['Reduction'] = True + + shield = ba.newnode('shield',owner=self.node, + attrs={'color': (4,1,4),'radius': 1.3}) + self.node.connectattr('position_center', shield, 'position') + + self.tankshield['Shield'] = shield + except: pass + +Spaz._super_on_punch_release = Spaz.on_punch_release +def spaz_on_punch_release(self) -> None: + self._super_on_punch_release() + try: + self.tankshield['Shield'].delete() + self.tankshield['Reduction'] = False + except: pass + +def new_get_bomb_type_tex(self) -> ba.Texture: + factory = NewPowerupBoxFactory.get() + if self.bomb_type == 'sticky': + return factory.tex_sticky_bombs + if self.bomb_type == 'ice': + return factory.tex_ice_bombs + if self.bomb_type == 'impact': + return factory.tex_impact_bombs + if self.bomb_type == 'impairment': + return factory.tex_impairment_bombs + if self.bomb_type == 'fire': + return factory.tex_fire_bombs + if self.bomb_type == 'fly': + return factory.tex_fly_bombs + return factory.tex_impact_bombs + # raise ValueError('invalid bomb type') + +def new_handlemessage(self, msg: Any) -> Any: + assert not self.expired + + if isinstance(msg, ba.PickedUpMessage): + if self.node: + self.node.handlemessage('hurt_sound') + self.node.handlemessage('picked_up') + + self._num_times_hit += 1 + + elif isinstance(msg, ba.ShouldShatterMessage): + ba.timer(0.001, ba.Call(self.shatter)) + + elif isinstance(msg, ba.ImpactDamageMessage): + ba.timer(0.001, ba.Call(self._hit_self, msg.intensity)) + + elif isinstance(msg, ba.PowerupMessage): + factory = NewPowerupBoxFactory.get() + if self._dead or not self.node: + return True + if self.pick_up_powerup_callback is not None: + self.pick_up_powerup_callback(self) + if msg.poweruptype == 'triple_bombs': + tex = PowerupBoxFactory.get().tex_bomb + self._flash_billboard(tex) + self.set_bomb_count(3) + if self.powerups_expire: + self.node.mini_billboard_1_texture = tex + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + self.node.mini_billboard_1_start_time = t_ms + self.node.mini_billboard_1_end_time = ( + t_ms + POWERUP_WEAR_OFF_TIME) + self._multi_bomb_wear_off_timer = (ba.Timer( + (POWERUP_WEAR_OFF_TIME - 2000), + ba.Call(self._multi_bomb_wear_off_flash), + timeformat=ba.TimeFormat.MILLISECONDS)) + self._multi_bomb_wear_off_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME, + ba.Call(self._multi_bomb_wear_off), + timeformat=ba.TimeFormat.MILLISECONDS)) + elif msg.poweruptype == 'land_mines': + self.set_land_mine_count(min(self.land_mine_count + 3, 3)) + elif msg.poweruptype == 'impact_bombs': + self.bomb_type = 'impact' + tex = self._get_bomb_type_tex() + self._flash_billboard(tex) + if self.powerups_expire: + self.node.mini_billboard_2_texture = tex + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = ( + t_ms + POWERUP_WEAR_OFF_TIME) + self._bomb_wear_off_flash_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME - 2000, + ba.Call(self._bomb_wear_off_flash), + timeformat=ba.TimeFormat.MILLISECONDS)) + self._bomb_wear_off_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME, + ba.Call(self._bomb_wear_off), + timeformat=ba.TimeFormat.MILLISECONDS)) + elif msg.poweruptype == 'sticky_bombs': + self.bomb_type = 'sticky' + tex = self._get_bomb_type_tex() + self._flash_billboard(tex) + if self.powerups_expire: + self.node.mini_billboard_2_texture = tex + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = ( + t_ms + POWERUP_WEAR_OFF_TIME) + self._bomb_wear_off_flash_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME - 2000, + ba.Call(self._bomb_wear_off_flash), + timeformat=ba.TimeFormat.MILLISECONDS)) + self._bomb_wear_off_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME, + ba.Call(self._bomb_wear_off), + timeformat=ba.TimeFormat.MILLISECONDS)) + elif msg.poweruptype == 'punch': + self._has_boxing_gloves = True + tex = PowerupBoxFactory.get().tex_punch + self._flash_billboard(tex) + self.equip_boxing_gloves() + if self.powerups_expire: + self.node.boxing_gloves_flashing = False + self.node.mini_billboard_3_texture = tex + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + self.node.mini_billboard_3_start_time = t_ms + self.node.mini_billboard_3_end_time = ( + t_ms + POWERUP_WEAR_OFF_TIME) + self._boxing_gloves_wear_off_flash_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME - 2000, + ba.WeakCall(self._gloves_wear_off_flash), + timeformat=ba.TimeFormat.MILLISECONDS)) + self._boxing_gloves_wear_off_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME, + ba.WeakCall(self._gloves_wear_off), + timeformat=ba.TimeFormat.MILLISECONDS)) + elif msg.poweruptype == 'shield': + factory = SpazFactory.get() + self.equip_shields(decay=factory.shield_decay_rate > 0) + elif msg.poweruptype == 'curse': + self.curse() + elif msg.poweruptype == 'ice_bombs': + self.bomb_type = 'ice' + tex = self._get_bomb_type_tex() + self._flash_billboard(tex) + if self.powerups_expire: + self.node.mini_billboard_2_texture = tex + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = ( + t_ms + POWERUP_WEAR_OFF_TIME) + self._bomb_wear_off_flash_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME - 2000, + ba.WeakCall(self._bomb_wear_off_flash), + timeformat=ba.TimeFormat.MILLISECONDS)) + self._bomb_wear_off_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME, + ba.WeakCall(self._bomb_wear_off), + timeformat=ba.TimeFormat.MILLISECONDS)) + elif msg.poweruptype == 'health': + if self.edg_eff: + f = self.color[0] + r = (2,0,0) + g = (0,2,0) + ba.animate_array(self.node,'color',3,{0: r, 0.6: g, 1.0: f}) + self.edg_eff = False + if self._cursed: + self._cursed = False + factory = SpazFactory.get() + for attr in ['materials', 'roller_materials']: + materials = getattr(self.node, attr) + if factory.curse_material in materials: + setattr( + self.node, attr, + tuple(m for m in materials + if m != factory.curse_material)) + self.node.curse_death_time = 0 + self.hitpoints = self.hitpoints_max + self._flash_billboard(PowerupBoxFactory.get().tex_health) + self.node.hurt = 0 + self._last_hit_time = None + self._num_times_hit = 0 + + elif msg.poweruptype == 'tank_shield': + self.tankshield['Tank'] = True + self.edg_eff = False + tex = factory.tex_tank_shield + self._flash_billboard(tex) + + elif msg.poweruptype == 'health_damage': + tex = factory.tex_health_damage + self.edg_eff = True + f = self.color[0] + i = (2,0.5,2) + ba.animate_array(self.node,'color',3,{0: i, 0.5: i, 0.6: f}) + self._flash_billboard(tex) + self.tankshield['Tank'] = False + self.freeze_punch = False + + elif msg.poweruptype == 'goodbye': + tex = factory.tex_goodbye + self._flash_billboard(tex) + self.kill_eff = True + + elif msg.poweruptype == 'fly_bombs': + self.bomb_type = 'fly' + tex = self._get_bomb_type_tex() + self._flash_billboard(tex) + if self.powerups_expire: + self.node.mini_billboard_2_texture = tex + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = ( + t_ms + POWERUP_WEAR_OFF_TIME) + self._bomb_wear_off_flash_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME - 2000, + ba.WeakCall(self._bomb_wear_off_flash), + timeformat=ba.TimeFormat.MILLISECONDS)) + self._bomb_wear_off_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME, + ba.WeakCall(self._bomb_wear_off), + timeformat=ba.TimeFormat.MILLISECONDS)) + + elif msg.poweruptype == 'fire_bombs': + self.bomb_type = 'fire' + tex = self._get_bomb_type_tex() + self._flash_billboard(tex) + if self.powerups_expire: + self.node.mini_billboard_2_texture = tex + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = ( + t_ms + POWERUP_WEAR_OFF_TIME) + self._bomb_wear_off_flash_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME - 2000, + ba.WeakCall(self._bomb_wear_off_flash), + timeformat=ba.TimeFormat.MILLISECONDS)) + self._bomb_wear_off_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME, + ba.WeakCall(self._bomb_wear_off), + timeformat=ba.TimeFormat.MILLISECONDS)) + + elif msg.poweruptype == 'impairment_bombs': + self.bomb_type = 'impairment' + tex = self._get_bomb_type_tex() + self._flash_billboard(tex) + if self.powerups_expire: + self.node.mini_billboard_2_texture = tex + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = ( + t_ms + POWERUP_WEAR_OFF_TIME) + self._bomb_wear_off_flash_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME - 2000, + ba.WeakCall(self._bomb_wear_off_flash), + timeformat=ba.TimeFormat.MILLISECONDS)) + self._bomb_wear_off_timer = (ba.Timer( + POWERUP_WEAR_OFF_TIME, + ba.WeakCall(self._bomb_wear_off), + timeformat=ba.TimeFormat.MILLISECONDS)) + + elif msg.poweruptype == 'ice_man': + tex = factory.tex_ice_man + self.bomb_type = 'ice_bubble' + self.freeze_punch = True + self.edg_eff = False + self.node.color = (0,1,4) + self._flash_billboard(tex) + + if self.powerups_expire: + ice_man_time = 17000 + self.node.mini_billboard_2_texture = tex + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = (t_ms + ice_man_time) + + self.ice_man_flash_timer = (ba.Timer( + ice_man_time - 2000, + ba.Call(_ice_man_off_flash,self), + timeformat=ba.TimeFormat.MILLISECONDS)) + + self.ice_man_timer = (ba.Timer(ice_man_time, + ba.Call(_ice_man_wear_off,self), + timeformat=ba.TimeFormat.MILLISECONDS)) + + elif msg.poweruptype == 'speed': + self.node.hockey = True + tex = factory.tex_speed + self._flash_billboard(tex) + if self.powerups_expire: + + speed_time = 15000 + self.node.mini_billboard_2_texture = tex + t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(t_ms, int) + self.node.mini_billboard_2_start_time = t_ms + self.node.mini_billboard_2_end_time = (t_ms + speed_time) + + self.speed_flash_timer = (ba.Timer( + speed_time - 2000, + ba.Call(_speed_off_flash,self), + timeformat=ba.TimeFormat.MILLISECONDS)) + + self.speed_timer = (ba.Timer(speed_time, + ba.Call(_speed_wear_off,self), + timeformat=ba.TimeFormat.MILLISECONDS)) + + self.bmb_color: list = [] + self.bmb_color.append(self.bomb_type) + + self.node.handlemessage('flash') + if msg.sourcenode: + msg.sourcenode.handlemessage(ba.PowerupAcceptMessage()) + return True + + elif isinstance(msg, ba.FreezeMessage): + if not self.node: + return None + if self.node.invincible: + ba.playsound(SpazFactory.get().block_sound, + 1.0, + position=self.node.position) + return None + if self.shield: + return None + if not self.frozen: + self.frozen = True + self.node.frozen = True + ba.timer(5.0, ba.Call(self.handlemessage, + ba.ThawMessage())) + if self.hitpoints <= 0: + self.shatter() + if self.freeze_punch: + self.handlemessage(ba.ThawMessage()) + + elif isinstance(msg, ba.ThawMessage): + if self.frozen and not self.shattered and self.node: + self.frozen = False + self.node.frozen = False + + elif isinstance(msg, ba.HitMessage): + if not self.node: + return None + if self.node.invincible: + ba.playsound(SpazFactory.get().block_sound, + 1.0, + position=self.node.position) + return True + + local_time = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) + assert isinstance(local_time, int) + if (self._last_hit_time is None + or local_time - self._last_hit_time > 1000): + self._num_times_hit += 1 + self._last_hit_time = local_time + + mag = msg.magnitude * self.impact_scale + velocity_mag = msg.velocity_magnitude * self.impact_scale + damage_scale = 0.22 + + def fire_effect(): + if not self.shield: + if self.node.exists(): + ba.emitfx(position=self.node.position, + scale=3,count=50*2,spread=0.3, + chunk_type='sweat') + self.node.handlemessage('celebrate', 560) + else: self._fire_time = None + else: self._fire_time = None + + def fire(time, damage): + if not self.shield and not self._dead: + self.hitpoints -= damage + ba.show_damage_count(f'-{damage}HP', + self.node.position, msg.force_direction) + ba.playsound(ba.getsound('fuse01')) + + if duration != time: + self._fire_time = ba.Timer(0.1,ba.Call(fire_effect),repeat=True) + else: self._fire_time = None + + if self.hitpoints < 0: + self.node.handlemessage(ba.DieMessage()) + + if msg.hit_subtype == 'fly': + damage_scale = 0.0 + + if self.shield: + self.shield_hitpoints -= 300 + + if self.shield_hitpoints < 0: + self.shield.delete() + self.shield = None + ba.playsound(SpazFactory.get().shield_down_sound,1.0,position=self.node.position) + elif msg.hit_subtype == 'fire': + index = 1 + duration = 5 + damage = 103 + if not self.shield: + for firex in range(duration): + ba.timer(index,ba.Call(fire,index,damage)) + self._fire_time = ba.Timer(0.1,ba.Call(fire_effect),repeat=True) + index += 1 + else: + self.shield_hitpoints -= 80 + if self.shield_hitpoints < 1: + self.shield.delete() + self.shield = None + ba.playsound(SpazFactory.get().shield_down_sound,1.0,position=self.node.position) + elif msg.hit_subtype == 'impairment': + damage_scale = 0 + + if self.shield: + self.shield.delete() + self.shield = None + ba.playsound(SpazFactory.get().shield_down_sound,1.0,position=self.node.position) + else: + hitpoints = int(self.hitpoints*0.80) + self.hitpoints -= int(hitpoints) + ba.show_damage_count((f'-{int(hitpoints/10)}%'), + self.node.position, msg.force_direction) + + if self.hitpoints < 0 or hitpoints < 95: + self.node.handlemessage(ba.DieMessage()) + + if self.shield: + if msg.flat_damage: + damage = msg.flat_damage * self.impact_scale + else: + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], + msg.velocity[0], msg.velocity[1], msg.velocity[2], mag, + velocity_mag, msg.radius, 1, msg.force_direction[0], + msg.force_direction[1], msg.force_direction[2]) + damage = damage_scale * self.node.damage + + assert self.shield_hitpoints is not None + self.shield_hitpoints -= int(damage) + self.shield.hurt = ( + 1.0 - + float(self.shield_hitpoints) / self.shield_hitpoints_max) + + max_spillover = SpazFactory.get().max_shield_spillover_damage + if self.shield_hitpoints <= 0: + + self.shield.delete() + self.shield = None + ba.playsound(SpazFactory.get().shield_down_sound, + 1.0, + position=self.node.position) + + npos = self.node.position + ba.emitfx(position=(npos[0], npos[1] + 0.9, npos[2]), + velocity=self.node.velocity, + count=random.randrange(20, 30), + scale=1.0, + spread=0.6, + chunk_type='spark') + + else: + ba.playsound(SpazFactory.get().shield_hit_sound, + 0.5, + position=self.node.position) + + assert msg.force_direction is not None + ba.emitfx(position=msg.pos, + velocity=(msg.force_direction[0] * 1.0, + msg.force_direction[1] * 1.0, + msg.force_direction[2] * 1.0), + count=min(30, 5 + int(damage * 0.005)), + scale=0.5, + spread=0.3, + chunk_type='spark') + + if self.shield_hitpoints <= -max_spillover: + leftover_damage = -max_spillover - self.shield_hitpoints + shield_leftover_ratio = leftover_damage / damage + + mag *= shield_leftover_ratio + velocity_mag *= shield_leftover_ratio + else: + return True + else: + shield_leftover_ratio = 1.0 + + if msg.flat_damage: + damage = int(msg.flat_damage * self.impact_scale * + shield_leftover_ratio) + else: + assert msg.force_direction is not None + self.node.handlemessage( + 'impulse', msg.pos[0], msg.pos[1], msg.pos[2], + msg.velocity[0], msg.velocity[1], msg.velocity[2], mag, + velocity_mag, msg.radius, 0, msg.force_direction[0], + msg.force_direction[1], msg.force_direction[2]) + + damage = int(damage_scale * self.node.damage) + + if self.tankshield['Reduction']: + porcentaje = percentage_tank_shield() + dism = int(damage*porcentaje) + damage = int(damage-dism) + + ba.show_damage_count('-' + str(int(damage / 10)) + '%', + msg.pos, msg.force_direction) + + self.node.handlemessage('hurt_sound') + + if self.edg_eff: + porcentaje = percentage_health_damage() + dmg_dism = int(damage*porcentaje) + self.hitpoints += dmg_dism + + PopupText(text=f'+{int(dmg_dism/10)}%',scale=1.5, + position=self.node.position,color=(0,1,0)).autoretain() + ba.animate_array(self.node,'color',3,{0: (0,1,0), 0.39: (0,2,0), 0.4: self.color[0]}) + ba.playsound(ba.getsound('healthPowerup')) + + if msg.hit_type == 'punch': + self.on_punched(damage) + + try: + if msg.get_source_player(ba.Player).actor.freeze_punch: + self.node.color = (0,1,4) + ba.playsound(ba.getsound('freeze')) + self.node.handlemessage(ba.FreezeMessage()) + except: pass + + if damage > 350: + assert msg.force_direction is not None + ba.show_damage_count('-' + str(int(damage / 10)) + '%', + msg.pos, msg.force_direction) + + if msg.hit_subtype == 'super_punch': + ba.playsound(SpazFactory.get().punch_sound_stronger, + 1.0, + position=self.node.position) + if damage > 500: + sounds = SpazFactory.get().punch_sound_strong + sound = sounds[random.randrange(len(sounds))] + else: + sound = SpazFactory.get().punch_sound + ba.playsound(sound, 1.0, position=self.node.position) + + assert msg.force_direction is not None + ba.emitfx(position=msg.pos, + velocity=(msg.force_direction[0] * 0.5, + msg.force_direction[1] * 0.5, + msg.force_direction[2] * 0.5), + count=min(10, 1 + int(damage * 0.0025)), + scale=0.3, + spread=0.03) + + ba.emitfx(position=msg.pos, + chunk_type='sweat', + velocity=(msg.force_direction[0] * 1.3, + msg.force_direction[1] * 1.3 + 5.0, + msg.force_direction[2] * 1.3), + count=min(30, 1 + int(damage * 0.04)), + scale=0.9, + spread=0.28) + + hurtiness = damage * 0.003 + punchpos = (msg.pos[0] + msg.force_direction[0] * 0.02, + msg.pos[1] + msg.force_direction[1] * 0.02, + msg.pos[2] + msg.force_direction[2] * 0.02) + flash_color = (1.0, 0.8, 0.4) + light = ba.newnode( + 'light', + attrs={ + 'position': punchpos, + 'radius': 0.12 + hurtiness * 0.12, + 'intensity': 0.3 * (1.0 + 1.0 * hurtiness), + 'height_attenuated': False, + 'color': flash_color + }) + ba.timer(0.06, light.delete) + + flash = ba.newnode('flash', + attrs={ + 'position': punchpos, + 'size': 0.17 + 0.17 * hurtiness, + 'color': flash_color + }) + ba.timer(0.06, flash.delete) + + if msg.hit_type == 'impact': + assert msg.force_direction is not None + ba.emitfx(position=msg.pos, + velocity=(msg.force_direction[0] * 2.0, + msg.force_direction[1] * 2.0, + msg.force_direction[2] * 2.0), + count=min(10, 1 + int(damage * 0.01)), + scale=0.4, + spread=0.1) + if self.hitpoints > 0: + if msg.hit_type == 'impact' and damage > self.hitpoints: + newdamage = max(damage - 200, self.hitpoints - 10) + damage = newdamage + self.node.handlemessage('flash') + + if damage > 0.0 and self.node.hold_node: + self.node.hold_node = None + self.hitpoints -= damage + self.node.hurt = 1.0 - float( + self.hitpoints) / self.hitpoints_max + + if self._cursed and damage > 0: + ba.timer( + 0.05, + ba.Call(self.curse_explode, + msg.get_source_player(ba.Player))) + + if self.frozen and (damage > 200 or self.hitpoints <= 0): + self.shatter() + elif self.hitpoints <= 0: + self.node.handlemessage( + ba.DieMessage(how=ba.DeathType.IMPACT)) + + if self.hitpoints <= 0: + damage_avg = self.node.damage_smoothed * damage_scale + if damage_avg > 1000: + self.shatter() + + elif isinstance(msg, BombDiedMessage): + self.bomb_count += 1 + + elif isinstance(msg, ba.DieMessage): + def drop_bomb(): + for xbomb in range(3): + p = self.node.position + pos = (p[0]+xbomb,p[1]+5,p[2]-xbomb) + ball = bomb.Bomb(position=pos,bomb_type='impact').autoretain() + ball.node.model_scale = 0.6 + ball.node.model = ba.getmodel('egg') + ball.node.gravity_scale = 2 + + if self.edg_eff: + self.edg_eff = False + + wasdead = self._dead + self._dead = True + self.hitpoints = 0 + if msg.immediate: + if self.node: + self.node.delete() + elif self.node: + self.node.hurt = 1.0 + if self.play_big_death_sound and not wasdead: + ba.playsound(SpazFactory.get().single_player_death_sound) + self.node.dead = True + ba.timer(2.0, self.node.delete) + + t = 0 + if self.kill_eff: + for bombs in range(3): + ba.timer(t,ba.Call(drop_bomb)) + t += 0.15 + self.kill_eff = False + + elif isinstance(msg, ba.OutOfBoundsMessage): + self.handlemessage(ba.DieMessage(how=ba.DeathType.FALL)) + + elif isinstance(msg, ba.StandMessage): + self._last_stand_pos = (msg.position[0], msg.position[1], + msg.position[2]) + if self.node: + self.node.handlemessage('stand', msg.position[0], + msg.position[1], msg.position[2], + msg.angle) + + elif isinstance(msg, CurseExplodeMessage): + self.curse_explode() + + elif isinstance(msg, PunchHitMessage): + if not self.node: + return None + node = ba.getcollision().opposingnode + + if node and (node not in self._punched_nodes): + + punch_momentum_angular = (self.node.punch_momentum_angular * + self._punch_power_scale) + punch_power = self.node.punch_power * self._punch_power_scale + + if node.getnodetype() != 'spaz': + sounds = SpazFactory.get().impact_sounds_medium + sound = sounds[random.randrange(len(sounds))] + ba.playsound(sound, 1.0, position=self.node.position) + + ppos = self.node.punch_position + punchdir = self.node.punch_velocity + vel = self.node.punch_momentum_linear + + self._punched_nodes.add(node) + node.handlemessage( + ba.HitMessage( + pos=ppos, + velocity=vel, + magnitude=punch_power * punch_momentum_angular * 110.0, + velocity_magnitude=punch_power * 40, + radius=0, + srcnode=self.node, + source_player=self.source_player, + force_direction=punchdir, + hit_type='punch', + hit_subtype=('super_punch' if self._has_boxing_gloves + else 'default'))) + + mag = -400.0 + if self._hockey: + mag *= 0.5 + if len(self._punched_nodes) == 1: + self.node.handlemessage('kick_back', ppos[0], ppos[1], + ppos[2], punchdir[0], punchdir[1], + punchdir[2], mag) + elif isinstance(msg, PickupMessage): + if not self.node: + return None + + try: + collision = ba.getcollision() + opposingnode = collision.opposingnode + opposingbody = collision.opposingbody + except ba.NotFoundError: + return True + + try: + if opposingnode.invincible: + return True + except Exception: + pass + + if (opposingnode.getnodetype() == 'spaz' + and not opposingnode.shattered and opposingbody == 4): + opposingbody = 1 + + held = self.node.hold_node + if held and held.getnodetype() == 'flag': + return True + + self.node.hold_body = opposingbody + self.node.hold_node = opposingnode + elif isinstance(msg, ba.CelebrateMessage): + if self.node: + self.node.handlemessage('celebrate', int(msg.duration * 1000)) + + return None + +class PowerupManagerWindow(PopupWindow): + def __init__(self, transition= 'in_right'): + columns = 2 + self._width = width = 800 + self._height = height = 500 + self._sub_height = 200 + self._scroll_width = self._width*0.90 + self._scroll_height = self._height - 180 + self._sub_width = self._scroll_width*0.95; + self.tab_buttons: set = {} + self.list_cls_power: list = [] + self.default_powerups = default_powerups() + self.default_power_list = list(self.default_powerups) + self.coins = apg['Bear Coin'] + self.popup_cls_power = None + + if not STORE['Buy Firebombs']: + powerups['Fire Bombs'] = 0 + self.default_power_list.remove('Fire Bombs') + + self.charstr = [ba.charstr(ba.SpecialChar.LEFT_ARROW), + ba.charstr(ba.SpecialChar.RIGHT_ARROW), + ba.charstr(ba.SpecialChar.UP_ARROW), + ba.charstr(ba.SpecialChar.DOWN_ARROW)] + + self.tabdefs = {"Action 1": ['powerupIceBombs',(1,1,1)], + "Action 2": ['settingsIcon',(0,1,0)], + "Action 3": ['inventoryIcon',(1,1,1)], + "Action 4": ['storeIcon',(1,1,1)], + "Action 5": ['advancedIcon',(1,1,1)], + "About": ['heart',(1.5,0.3,0.3)]} + + if (STORE['Buy Firebombs'] and + STORE['Buy Option'] and + STORE['Buy Percentage']): + self.tabdefs = {"Action 1": ['powerupIceBombs',(1,1,1)], + "Action 2": ['settingsIcon',(0,1,0)], + "Action 3": ['inventoryIcon',(1,1,1)], + "About": ['heart',(1.5,0.3,0.3)]} + + self.listdef = list(self.tabdefs) + + self.count = len(self.tabdefs) + + self._current_tab = GLOBAL['Tab'] + + app = ba.app.ui + uiscale = app.uiscale + + self._root_widget = ba.containerwidget(size=(width+90,height+80),transition=transition, + scale=1.5 if uiscale is ba.UIScale.SMALL else 1.0, + stack_offset=(0,-30) if uiscale is ba.UIScale.SMALL else (0,0)) + + self._backButton = b = ba.buttonwidget(parent=self._root_widget,autoselect=True, + position=(60,self._height-15),size=(130,60), + scale=0.8,text_scale=1.2,label=ba.Lstr(resource='backText'), + button_type='back',on_activate_call=ba.Call(self._back)) + ba.buttonwidget(edit=self._backButton, button_type='backSmall',size=(60, 60),label=ba.charstr(ba.SpecialChar.BACK)) + ba.containerwidget(edit=self._root_widget,cancel_button=b) + + self.titletext = ba.textwidget(parent=self._root_widget,position=(0, height-15),size=(width,50), + h_align="center",color=ba.app.ui.title_color, v_align="center",maxwidth=width*1.3) + + index = 0 + for tab in range(self.count): + for tab2 in range(columns): + + tag = self.listdef[index] + + position = (620+(tab2*120),self._height-50*2.5-(tab*120)) + + if tag == 'About': + text = ba.Lstr(resource='gatherWindow.aboutText') + elif tab == 'Action 4': + text = ba.Lstr(resource='storeText') + else: text = getlanguage(tag) + + self.tab_buttons[tag] = ba.buttonwidget(parent=self._root_widget,autoselect=True, + position=position,size=(110,110), + scale=1,label='',enable_sound=False, + button_type='square',on_activate_call=ba.Call(self._set_tab,tag,sound=True)) + + self.text = ba.textwidget(parent=self._root_widget, + position=(position[0]+55,position[1]+30), + size=(0, 0),scale=1,color=ba.app.ui.title_color, + draw_controller=self.tab_buttons[tag],maxwidth=100, + text=text,h_align='center',v_align='center') + + self.image = ba.imagewidget(parent=self._root_widget, + size=(60,60),color=self.tabdefs[tag][1], + draw_controller=self.tab_buttons[tag], + position=(position[0]+25,position[1]+40), + texture=ba.gettexture(self.tabdefs[tag][0])) + + index += 1 + + if self.count == index: + break + + if self.count == index: + break + + self._scrollwidget = None + self._tab_container = None + self._set_tab(self._current_tab) + + def __del__(self): + apg.apply_and_commit() + + def _set_tab(self, tab, sound: bool = False): + self.sound = sound + GLOBAL['Tab'] = tab + apg.apply_and_commit() + + if self._tab_container is not None and self._tab_container.exists(): + self._tab_container.delete() + + if self.sound: + ba.playsound(ba.getsound('click01')) + + if self._scrollwidget: + self._scrollwidget.delete() + + self._scrollwidget = ba.scrollwidget(parent=self._root_widget, + position=(self._width*0.08,51*1.8),size=(self._sub_width -140,self._scroll_height +60*1.2)) + + if tab == 'Action 4': + if self._scrollwidget: + self._scrollwidget.delete() + self._scrollwidget = ba.hscrollwidget(parent=self._root_widget, + position=(self._width*0.08,51*1.8),size=(self._sub_width -140,self._scroll_height +60*1.2), + capture_arrows=True,claims_left_right=True) + ba.textwidget(edit=self.titletext,text=ba.Lstr(resource='storeText')) + elif tab == 'About': + ba.textwidget(edit=self.titletext,text=ba.Lstr(resource='gatherWindow.aboutText')) + else: ba.textwidget(edit=self.titletext,text=getlanguage(tab)) + + choices = ['Reset','Only Bombs','Only Items','New','Nothing'] + c_display = [] + + for display in choices: + choices_display = ba.Lstr(translate=("",getlanguage(display))) + c_display.append(choices_display) + + if tab == 'Action 1': + self.popup_cls_power = PopupMenu( + parent=self._root_widget, + position=(130,self._width*0.61), + button_size=(150,50),scale=2.5, + choices=choices,width=150, + choices_display=c_display, + current_choice=GLOBAL['Cls Powerup'], + on_value_change_call=self._set_concept) + self.list_cls_power.append(self.popup_cls_power._button) + + self.button_cls_power = ba.buttonwidget(parent=self._root_widget, + position=(500,self._width*0.61),size=(50,50),autoselect=True, + scale=1,label=('%'),text_scale=1,button_type='square', + on_activate_call=self._percentage_window) + self.list_cls_power.append(self.button_cls_power) + + rewindow = [self.popup_cls_power._button,self.button_cls_power] + + for cls in self.list_cls_power: # this is very important so that pupups don't accumulate + if cls not in rewindow: + cls.delete() + + elif tab == 'Action 4': + self.button_coin = ba.buttonwidget(parent=self._root_widget,icon=ba.gettexture('coin'), + position=(550,self._width*0.614),size=(160,40),textcolor=(0,1,0),color=(0,1,6), + scale=1,label=str(apg['Bear Coin']),text_scale=1,autoselect=True, + on_activate_call=None) #self._percentage_window) + self.list_cls_power.append(self.button_coin) + + try: rewindow.append(self.button_coin) + except: rewindow = [self.button_coin] + for cls in self.list_cls_power: # this is very important so that pupups don't accumulate + if cls not in rewindow: + cls.delete() + + else: + try: + for cls in self.list_cls_power: + cls.delete() + except: pass + + if tab == 'Action 1': + sub_height = len(self.default_power_list) * 90 + v = sub_height - 55 + width = 300 + posi = 0 + id_power = list(self.default_powerups) + new_powerups = id_power[9:] + self.listpower = {} + + self._tab_container = c = ba.containerwidget(parent=self._scrollwidget, + size=(self._sub_width,sub_height), + background=False,selection_loops_to_parent=True) + + for power in self.default_power_list: + if power == id_power[0]: + text = 'helpWindow.powerupShieldNameText' + tex = ba.gettexture('powerupShield') + elif power == id_power[1]: + text = 'helpWindow.powerupPunchNameText' + tex = ba.gettexture('powerupPunch') + elif power == id_power[2]: + text = 'helpWindow.powerupLandMinesNameText' + tex = ba.gettexture('powerupLandMines') + elif power == id_power[3]: + text = 'helpWindow.powerupImpactBombsNameText' + tex = ba.gettexture('powerupImpactBombs') + elif power == id_power[4]: + text = 'helpWindow.powerupIceBombsNameText' + tex = ba.gettexture('powerupIceBombs') + elif power == id_power[5]: + text = 'helpWindow.powerupBombNameText' + tex = ba.gettexture('powerupBomb') + elif power == id_power[6]: + text = 'helpWindow.powerupStickyBombsNameText' + tex = ba.gettexture('powerupStickyBombs') + elif power == id_power[7]: + text = 'helpWindow.powerupCurseNameText' + tex = ba.gettexture('powerupCurse') + elif power == id_power[8]: + text = 'helpWindow.powerupHealthNameText' + tex = ba.gettexture('powerupHealth') + elif power == id_power[9]: + text = power + tex = ba.gettexture('powerupSpeed') + elif power == id_power[10]: + text = power + tex = ba.gettexture('heart') + elif power == id_power[11]: + text = "Goodbye!" + tex = ba.gettexture('achievementOnslaught') + elif power == id_power[12]: + text = power + tex = ba.gettexture('ouyaUButton') + elif power == id_power[13]: + text = power + tex = ba.gettexture('achievementSuperPunch') + elif power == id_power[14]: + text = power + tex = ba.gettexture('levelIcon') + elif power == id_power[15]: + text = power + tex = ba.gettexture('ouyaOButton') + elif power == id_power[16]: + text = power + tex = ba.gettexture('star') + + if power in new_powerups: label = getlanguage(power) + else: label = ba.Lstr(resource=text) + + apperance = powerups[power] + position = (90,v-posi) + + t = ba.textwidget(parent=c,position=(position[0]-30,position[1]-15),size=(width,50), + h_align="center",color=(ba.app.ui.title_color), text=label, v_align="center",maxwidth=width*1.3) + + self.powprev = ba.imagewidget(parent=c, + position=(position[0]-70,position[1]-10), + size=(50,50),texture=tex) + + dipos = 0 + for direc in ['-','+']: + ba.buttonwidget(parent=c,autoselect=True, + position=(position[0]+270+dipos,position[1]-10),size=(100,100), + scale=0.4,label=direc,button_type='square',text_scale=4, + on_activate_call=ba.Call(self.apperance_powerups,power,direc)) + dipos += 100 + + textwidget = ba.textwidget(parent=c,position=(position[0]+190,position[1]-15),size=(width,50), + h_align="center",color=cls_pow_color()[apperance],text=str(apperance), + v_align="center",maxwidth=width*1.3) + self.listpower[power] = textwidget + + posi += 90 + + elif tab == 'Action 2': + sub_height = 370 if not STORE['Buy Option'] else 450 + v = sub_height - 55 + width = 300 + + self._tab_container = c = ba.containerwidget(parent=self._scrollwidget, + size=(self._sub_width,sub_height), + background=False,selection_loops_to_parent=True) + + position = (40,v-20) + + c_display = [] + choices = ['Auto','SY: BALL','SY: Impact','SY: Egg'] + for display in choices: + choices_display = ba.Lstr(translate=("",getlanguage(display))) + c_display.append(choices_display) + + popup = PopupMenu(parent=c, + position=(position[0]+300,position[1]), + button_size=(150,50),scale=2.5, + choices=choices,width=150, + choices_display=c_display, + current_choice=config['Powerup Style'], + on_value_change_call=ba.Call(self._all_popup,'Powerup Style')) + + text = getlanguage('Powerup Style') + wt = (len(text)*0.80) + t = ba.textwidget(parent=c,position=(position[0]-60+wt,position[1]),size=(width,50),maxwidth=width*0.9, + scale=1.1,h_align="center",color=ba.app.ui.title_color,text=getlanguage('Powerup Style'),v_align="center") + + dipos = 0 + for direc in ['-','+']: + ba.buttonwidget(parent=c,autoselect=True, + position=(position[0]+310+dipos,position[1]-100),size=(100,100), + repeat=True,scale=0.4,label=direc,button_type='square',text_scale=4, + on_activate_call=ba.Call(self._powerups_scale,direc)) + dipos += 100 + + txt_scale = config['Powerup Scale'] + self.txt_scale = ba.textwidget(parent=c,position=(position[0]+230,position[1]-105),size=(width,50), + scale=1.1,h_align="center",color=(0,1,0),text=str(txt_scale),v_align="center",maxwidth=width*1.3) + + text = getlanguage('Powerup Scale') + wt = (len(text)*0.80) + t = ba.textwidget(parent=c,position=(position[0]-60+wt,position[1]-100),size=(width,50),maxwidth=width*0.9, + scale=1.1,h_align="center",color=ba.app.ui.title_color,text=text,v_align="center") + + position = (position[0]-20,position[1]+40) + + self.check = ba.checkboxwidget(parent=c,position=(position[0]+30,position[1]-230),value=config['Powerup Name'], + on_value_change_call=ba.Call(self._switches,'Powerup Name'),maxwidth=self._scroll_width*0.9, + text=getlanguage('Powerup Name'),autoselect=True) + + self.check = ba.checkboxwidget(parent=c,position=(position[0]+30,position[1]-230*1.3),value=config['Powerup With Shield'], + on_value_change_call=ba.Call(self._switches,'Powerup With Shield'),maxwidth=self._scroll_width*0.9, + text=getlanguage('Powerup With Shield'),autoselect=True) + + if STORE['Buy Option']: + self.check = ba.checkboxwidget(parent=c,position=(position[0]+30,position[1]-230*1.6),value=config['Powerup Time'], + on_value_change_call=ba.Call(self._switches,'Powerup Time'),maxwidth=self._scroll_width*0.9, + text=getlanguage('Powerup Time'),autoselect=True) + + elif tab == 'Action 3': + sub_height = 300 + v = sub_height - 55 + width = 300 + + self._tab_container = c = ba.containerwidget(parent=self._scrollwidget, + size=(self._sub_width,sub_height), + background=False,selection_loops_to_parent=True) + + v -= 20 + position = (110,v-45*1.72) + + if not STORE['Buy Percentage']: + t = ba.textwidget(parent=c,position=(90,v-100),size=(30+width,50), + h_align="center",text=getlanguage('Block Option Store'), + color=ba.app.ui.title_color,v_align="center",maxwidth=width*1.5,scale=1.5) + + i = ba.imagewidget(parent=c, + position=(position[0]+100,position[1]-205), + size=(80,80),texture=ba.gettexture('lock')) + else: + t = ba.textwidget(parent=c,position=(position[0]-14,position[1]+70),size=(30+width,50), + h_align="center",text=f"{getlanguage('Tank Shield PTG')} ({getlanguage('Tank Shield')})", + color=ba.app.ui.title_color,v_align="center",maxwidth=width*1.5,scale=1.5) + + b = ba.buttonwidget(parent=c,autoselect=True,position=position,size=(100,100),repeat=True, + scale=0.6,label=self.charstr[3],button_type='square',text_scale=2, + on_activate_call=ba.Call(self.tank_shield_percentage,'Decrement')) + + b = ba.buttonwidget(parent=c,autoselect=True,repeat=True,text_scale=2, + position=(position[0]*3.2,position[1]),size=(100,100), + scale=0.6,label=self.charstr[2],button_type='square', + on_activate_call=ba.Call(self.tank_shield_percentage,'Increment')) + + porcentaje = config['Tank Shield PTG'] + if porcentaje > 59: color = (0,1,0) + elif porcentaje < 40: color = (1,1,0) + else: color = (0,1,0.8) + + self.tank_text = ba.textwidget(parent=c,position=(position[0]-14,position[1]+5), + size=(30+width,50),h_align="center", + text=str(porcentaje)+'%',color=color, + v_align="center",maxwidth=width*1.3,scale=2) + + # -----> + + position = (110,v-160*1.6) + t = ba.textwidget(parent=c,position=(position[0]-14,position[1]+70),size=(30+width,50), + h_align="center",text=f"{getlanguage('Healing Damage PTG')}{_sp_}({getlanguage('Healing Damage')})", + color=ba.app.ui.title_color,v_align="center",maxwidth=width*1.3,scale=1.4) + + b = ba.buttonwidget(parent=c,autoselect=True,position=position,size=(100,100),repeat=True, + scale=0.6,label=self.charstr[3],button_type='square',text_scale=2, + on_activate_call=ba.Call(self.health_damage_percentage,'Decrement')) + + b = ba.buttonwidget(parent=c,autoselect=True,repeat=True,text_scale=2, + position=(position[0]*3.2,position[1]),size=(100,100), + scale=0.6,label=self.charstr[2],button_type='square', + on_activate_call=ba.Call(self.health_damage_percentage,'Increment')) + + porcentaje = config['Healing Damage PTG'] + if porcentaje > 59: color = (0,1,0) + elif porcentaje < 40: color = (1,1,0) + else: color = (0,1,0.8) + + self.hlg_text = ba.textwidget(parent=c,position=(position[0]-14,position[1]+5), + size=(30+width,50),h_align="center", + text=str(porcentaje)+'%',color=color, + v_align="center",maxwidth=width*1.3,scale=2) + + elif tab == 'Percentage': + sub_height = len(self.default_power_list) * 90 + v = sub_height - 55 + width = 300 + posi = 0 + id_power = list(self.default_powerups) + new_powerups = id_power[9:] + self.listpower = {} + + self._tab_container = c = ba.containerwidget(parent=self._scrollwidget, + size=(self._sub_width,sub_height), + background=False,selection_loops_to_parent=True) + + for power in self.default_power_list: + if power == id_power[0]: + text = 'helpWindow.powerupShieldNameText' + tex = ba.gettexture('powerupShield') + elif power == id_power[1]: + text = 'helpWindow.powerupPunchNameText' + tex = ba.gettexture('powerupPunch') + elif power == id_power[2]: + text = 'helpWindow.powerupLandMinesNameText' + tex = ba.gettexture('powerupLandMines') + elif power == id_power[3]: + text = 'helpWindow.powerupImpactBombsNameText' + tex = ba.gettexture('powerupImpactBombs') + elif power == id_power[4]: + text = 'helpWindow.powerupIceBombsNameText' + tex = ba.gettexture('powerupIceBombs') + elif power == id_power[5]: + text = 'helpWindow.powerupBombNameText' + tex = ba.gettexture('powerupBomb') + elif power == id_power[6]: + text = 'helpWindow.powerupStickyBombsNameText' + tex = ba.gettexture('powerupStickyBombs') + elif power == id_power[7]: + text = 'helpWindow.powerupCurseNameText' + tex = ba.gettexture('powerupCurse') + elif power == id_power[8]: + text = 'helpWindow.powerupHealthNameText' + tex = ba.gettexture('powerupHealth') + elif power == id_power[9]: + text = power + tex = ba.gettexture('powerupSpeed') + elif power == id_power[10]: + text = power + tex = ba.gettexture('heart') + elif power == id_power[11]: + text = "Goodbye!" + tex = ba.gettexture('achievementOnslaught') + elif power == id_power[12]: + text = power + tex = ba.gettexture('ouyaUButton') + elif power == id_power[13]: + text = power + tex = ba.gettexture('achievementSuperPunch') + elif power == id_power[14]: + text = power + tex = ba.gettexture('levelIcon') + elif power == id_power[15]: + text = power + tex = ba.gettexture('ouyaOButton') + elif power == id_power[16]: + text = power + tex = ba.gettexture('star') + + if power in new_powerups: label = getlanguage(power) + else: label = ba.Lstr(resource=text) + + apperance = powerups[power] + position = (90,v-posi) + + t = ba.textwidget(parent=c,position=(position[0]-30,position[1]-15),size=(width,50), + h_align="center",color=(ba.app.ui.title_color), text=label, v_align="center",maxwidth=width*1.3) + + self.powprev = ba.imagewidget(parent=c, + position=(position[0]-70,position[1]-10), + size=(50,50),texture=tex) + + ptg = str(self.total_percentage(power)) + t = ba.textwidget(parent=c,position=(position[0]+170,position[1]-10),size=(width,50), + h_align="center",color=(0,1,0),text=(f'{ptg}%'),v_align="center",maxwidth=width*1.3) + + posi += 90 + + elif tab == 'Action 4': + sub_height = 370 + width = 300 + v = sub_height - 55 + u = width - 60 + + self._tab_container = c = ba.containerwidget(parent=self._scrollwidget, + size=(width+500,sub_height), + background=False,selection_loops_to_parent=True) + + position = (u+150,v-250) + n_pos = 0 + prices = [7560, 5150, 3360] + str_name = ["FireBombs Store","Timer Store","Percentages Store"] + images = ["ouyaOButton","settingsIcon","inventoryIcon"] + + index = 0 + for store in store_items(): + p = prices[index] + txt = str_name[index] + label = getlanguage(txt) + tx_pos = len(label)*1.8 + lb_scale = len(label)*0.20 + preview = images[index] + + if STORE[store]: + text = getlanguage('Bought') + icon = ba.gettexture('graphicsIcon') + color = (0.52,0.48,0.63) + txt_scale = 1.5 + else: + text = str(p) + icon = ba.gettexture('coin') + color = (0.5,0.4,0.93) + txt_scale = 2 + + b = ba.buttonwidget(parent=c,autoselect=True,position=(position[0]+210-n_pos,position[1]), + size=(250,80),scale=0.7,label=text,text_scale=txt_scale,icon=icon,color=color, + iconscale=1.7,on_activate_call=ba.Call(self._buy_object,store,p)) + + s = 180 + b = ba.buttonwidget(parent=c,autoselect=True,position=(position[0]+210-n_pos,position[1]+55), + size=(s,s+30),scale=1,label='',color=color,button_type='square', + on_activate_call=ba.Call(self._buy_object,store,p)) + + s -= 80 + i = ba.imagewidget(parent=c,draw_controller=b, + position=(position[0]+250-n_pos,position[1]+140), + size=(s,s),texture=ba.gettexture(preview)) + + t = ba.textwidget(parent=c,position=(position[0]+270-n_pos,position[1]+101), + h_align="center",color=(ba.app.ui.title_color),text=label,v_align="center",maxwidth=130) + + n_pos += 280 + index += 1 + + elif tab == 'Action 5': + sub_height = 370 + v = sub_height - 55 + width = 300 + + self._tab_container = c = ba.containerwidget(parent=self._scrollwidget, + size=(self._sub_width,sub_height),background=False, + selection_loops_to_parent=True) + + position = (0,v-30) + + t = ba.textwidget(parent=c,position=(position[0]+80,position[1]-30),size=(width+60,50),scale=1, + h_align="center",color=(ba.app.ui.title_color),text=ba.Lstr( + resource='settingsWindowAdvanced.enterPromoCodeText'),v_align="center",maxwidth=width*1.3) + + self.promocode_text = ba.textwidget(parent=c,position=(position[0]+80,position[1]-100),size=(width+60,50),scale=1, + editable=True,h_align="center",color=(ba.app.ui.title_color),text='', v_align="center",maxwidth=width*1.3, + max_chars=30,description=ba.Lstr(resource='settingsWindowAdvanced.enterPromoCodeText')) + + self.promocode_button = ba.buttonwidget( + parent=c,position=(position[0]+160,position[1]-170), + size=(200, 60),scale=1.0,label=ba.Lstr(resource='submitText'), + on_activate_call=self._promocode) + + else: + sub_height = 0 + v = sub_height - 55 + width = 300 + + self._tab_container = c = ba.containerwidget(parent=self._scrollwidget, + size=(self._sub_width,sub_height), + background=False,selection_loops_to_parent=True) + + t = ba.textwidget(parent=c,position=(110, v-20),size=(width,50), + scale=1.4,color=(0.2,1.2,0.2),h_align="center",v_align="center", + text=("Ultimate Powerup Manager v1.7"),maxwidth=width*30) + + t = ba.textwidget(parent=c,position=(110, v-90),size=(width,50), + scale=1,color=(1.3,0.5,1.0),h_align="center",v_align="center", + text=getlanguage('Creator'),maxwidth=width*30) + + t = ba.textwidget(parent=c,position=(110, v-220),size=(width,50), + scale=1,color=(1.0,1.2,0.3),h_align="center",v_align="center", + text=getlanguage('Mod Info'),maxwidth=width*30) + + for select_tab,button_tab in self.tab_buttons.items(): + if select_tab == tab: + ba.buttonwidget(edit=button_tab,color=(0.5,0.4,1.5)) + else: ba.buttonwidget(edit=button_tab,color=(0.52,0.48,0.63)) + + def _all_popup(self, tag: str, popup: str) -> None: + config[tag] = popup + apg.apply_and_commit() + + def _set_concept(self, concept: str) -> None: + GLOBAL['Cls Powerup'] = concept + + if concept == 'Reset': + for power, deflt in default_powerups().items(): + powerups[power] = deflt + elif concept == 'Nothing': + for power in default_powerups(): + powerups[power] = 0 + elif concept == 'Only Bombs': + for power, deflt in default_powerups().items(): + if 'Bombs' not in power: + powerups[power] = 0 + else: powerups[power] = 3 + elif concept == 'Only Items': + for power, deflt in default_powerups().items(): + if 'Bombs' in power: + powerups[power] = 0 + else: powerups[power] = deflt + elif concept == 'New': + default_power = default_powerups() + new_powerups = list(default_power)[9:] + for power, deflt in default_power.items(): + if power not in new_powerups: + powerups[power] = 0 + else: powerups[power] = deflt + + if not STORE['Buy Firebombs']: + powerups['Fire Bombs'] = 0 + + self._set_tab('Action 1') + + def tank_shield_percentage(self, tag): + max = 96 + min = 40 + if tag == 'Increment': + config['Tank Shield PTG'] += 1 + if config['Tank Shield PTG'] > max: + config['Tank Shield PTG'] = min + elif tag == 'Decrement': + config['Tank Shield PTG'] -= 1 + if config['Tank Shield PTG'] < min: + config['Tank Shield PTG'] = max + + porcentaje = config['Tank Shield PTG'] + if porcentaje > 59: color = (0,1,0) + elif porcentaje < 40: color = (1,1,0) + else: color = (0,1,0.8) + ba.textwidget(edit=self.tank_text, + text=str(porcentaje)+'%',color=color) + + def health_damage_percentage(self, tag): + max = 80 + min = 35 + if tag == 'Increment': + config['Healing Damage PTG'] += 1 + if config['Healing Damage PTG'] > max: + config['Healing Damage PTG'] = min + elif tag == 'Decrement': + config['Healing Damage PTG'] -= 1 + if config['Healing Damage PTG'] < min: + config['Healing Damage PTG'] = max + + porcentaje = config['Healing Damage PTG'] + if porcentaje > 59: color = (0,1,0) + elif porcentaje < 40: color = (1,1,0) + else: color = (0,1,0.8) + ba.textwidget(edit=self.hlg_text, + text=str(porcentaje)+'%',color=color) + + def apperance_powerups(self, powerup: str, ID: str): + max = 7 + if ID == "-": + if powerups[powerup] == 0: + powerups[powerup] = max + else: powerups[powerup] -= 1 + elif ID == "+": + if powerups[powerup] == max: + powerups[powerup] = 0 + else: powerups[powerup] += 1 + enum = powerups[powerup] + ba.textwidget(edit=self.listpower[powerup], + text=str(powerups[powerup]), + color=cls_pow_color()[enum]) + + def _powerups_scale(self, ID: str): + max = 1.5 + min = 0.5 + sc = 0.1 + if ID == "-": + if config['Powerup Scale'] < (min+0.1): + config['Powerup Scale'] = max + else: config['Powerup Scale'] -= sc + elif ID == "+": + if config['Powerup Scale'] > (max-0.1): + config['Powerup Scale'] = min + else: config['Powerup Scale'] += sc + config['Powerup Scale'] = round(config['Powerup Scale'],1) + ba.textwidget(edit=self.txt_scale, + text=str(config['Powerup Scale'])) + + def total_percentage(self, power): + total = 0 + pw = powerups[power] + for i,i2 in powerups.items(): + total += i2 + if total == 0: + return float(total) + else: + ptg = (100*pw/total) + result = round(ptg,2) + return result + + def store_refresh(self, tag: str): + if tag == 'Buy Firebombs': + powerups['Fire Bombs'] = 3 + self.default_power_list.append('Fire Bombs') + self._set_tab('Action 4') + + def _buy_object(self, tag: str, price: int): + store = BearStore(value=tag, price=price, + callback=ba.Call(self.store_refresh,tag)) + store.buy() + + def _promocode(self): + code = ba.textwidget(query=self.promocode_text) + promo = PromoCode(code=code) + promo.code_confirmation() + ba.textwidget(edit=self.promocode_text,text="") + + def _switches(self,tag,m): + config[tag] = False if m==0 else True + apg.apply_and_commit() + + def _percentage_window(self): + self._set_tab('Percentage') + + def _back(self): + ba.containerwidget(edit=self._root_widget,transition='out_left') + browser.ProfileBrowserWindow() def enable(): diff --git a/dist/ba_root/mods/serverData/joining.log b/dist/ba_root/mods/serverData/joining.log new file mode 100644 index 0000000..111d4a7 --- /dev/null +++ b/dist/ba_root/mods/serverData/joining.log @@ -0,0 +1,4 @@ +2022-07-17 16:28:35.140169 + : PC452402||pb-IF4RU2ECAg==|| joined server +2022-07-17 18:36:28.993325 + : PC452402||pb-IF4RU2ECAg==|| joined server +2022-07-17 18:50:15.498297 + : PC452402||pb-IF4RU2ECAg==|| joined server +2022-07-17 18:56:26.791713 + : PC452402||pb-IF4RU2ECAg==|| joined server diff --git a/dist/ba_root/mods/setting.json b/dist/ba_root/mods/setting.json index 837970e..9ffbaa5 100644 --- a/dist/ba_root/mods/setting.json +++ b/dist/ba_root/mods/setting.json @@ -1,12 +1,12 @@ { "whitelist": false, - + "useV2Account": false, "ChatCommands": { "BrodcastCommand": true }, "textonmap": { "top watermark": "Welcome to server \nip 192.168.0.1", - "bottom left watermark": "Owner : \nEditor : \nScripts : BCS1.7", + "bottom left watermark": "Owner : \nEditor : \nScripts : BCS1.7.10", "center highlights":{ "color":[1,0,0], "randomColor":true, diff --git a/dist/ba_root/mods/spazmod/effects.py b/dist/ba_root/mods/spazmod/effects.py index ddb5de7..18a12d9 100644 --- a/dist/ba_root/mods/spazmod/effects.py +++ b/dist/ba_root/mods/spazmod/effects.py @@ -16,6 +16,7 @@ from bastd.actor import spaz,spazappearance from bastd.actor import bomb as stdbomb from bastd.actor.powerupbox import PowerupBoxFactory import ba,_ba,bastd,weakref,random,math,time,base64,os,json,setting +import ba.internal from playersData import pdata from stats import mystats PlayerType = TypeVar('PlayerType', bound=ba.Player) @@ -156,7 +157,7 @@ class Effect(ba.Actor): node_id = self.source_player.node.playerID cl_str = None clID = None - for c in _ba.get_foreground_host_session().sessionplayers: + for c in ba.internal.get_foreground_host_session().sessionplayers: if (c.activityplayer) and (c.activityplayer.node.playerID == node_id): profiles = c.inputdevice.get_player_profiles() clID = c.inputdevice.client_id @@ -247,7 +248,8 @@ class Effect(ba.Actor): ba.animate_array(self.scorchNode,"color",3,{0:self.scorchNode.color,500:color}, timetype=tt, timeformat=tf) else: self.scorchTimer = None - self.scorchNode.delete() + if hasattr(self,"scorchNode"): + self.scorchNode.delete() self.handlemessage(ba.DieMessage()) def neonLightSwitch(self,shine,Highlight,NameColor): diff --git a/dist/ba_root/mods/spazmod/hitmessage.py b/dist/ba_root/mods/spazmod/hitmessage.py index c1e03ba..eb7422f 100644 --- a/dist/ba_root/mods/spazmod/hitmessage.py +++ b/dist/ba_root/mods/spazmod/hitmessage.py @@ -2,6 +2,7 @@ # Released under the MIT License. See LICENSE for details. import ba, _ba, setting +import ba.internal from stats.mystats import damage_data from bastd.actor.popuptext import PopupText @@ -18,7 +19,7 @@ def handle_hit(msg, hp, dmg, hit_by, msg_pos): hit_by_id = hit_by.node.playerID if hit_by_id is not None: hit_by_account_id = None - for c in _ba.get_foreground_host_session().sessionplayers: + for c in ba.internal.get_foreground_host_session().sessionplayers: if (c.activityplayer) and (c.activityplayer.node.playerID == hit_by_id): hit_by_account_id = c.get_v1_account_id() if hit_by_account_id in damage_data: damage_data[hit_by_account_id] += float(dmg) diff --git a/dist/ba_root/mods/spazmod/modifyspaz.py b/dist/ba_root/mods/spazmod/modifyspaz.py index cbaf49d..0e5c7e9 100644 --- a/dist/ba_root/mods/spazmod/modifyspaz.py +++ b/dist/ba_root/mods/spazmod/modifyspaz.py @@ -4,13 +4,14 @@ import setting from random import randint import _ba,ba +import ba.internal _setting=setting.get_settings_data() def update_name(): - import _ba + import ba.internal from stats import mystats stat = mystats.get_all_stats() - ros = _ba.get_game_roster() + ros = ba.internal.get_game_roster() for i in ros: if i['account_id']: name = i['display_string'] @@ -60,7 +61,7 @@ def setTeamCharacter(): if not _setting["sameCharacterForTeam"]: return used=[] - teams=_ba.get_foreground_host_session().sessionteams + teams=ba.internal.get_foreground_host_session().sessionteams if len(teams) < 10: for team in teams: character=getRandomCharacter(used) diff --git a/dist/ba_root/mods/tools/ServerUpdate.py b/dist/ba_root/mods/tools/ServerUpdate.py index 3ff41dc..4ab8cfc 100644 --- a/dist/ba_root/mods/tools/ServerUpdate.py +++ b/dist/ba_root/mods/tools/ServerUpdate.py @@ -4,7 +4,7 @@ import _thread import urllib.request from efro.terminal import Clr import json -VERSION=69 +VERSION=71 def check(): _thread.start_new_thread(updateProfilesJson,()) @@ -25,7 +25,7 @@ def updateProfilesJson(): def fetchChangelogs(): url="https://raw.githubusercontent.com/imayushsaini/Bombsquad-Ballistica-Modded-Server/public-server/dist/ba_root/mods/changelogs.json" - + if 2*2==4: try: data=urllib.request.urlopen(url) @@ -58,7 +58,7 @@ def checkChangelog(): msg=changelog[log]["log"] print(f'{Clr.MAG} {msg} {Clr.RST}',flush=True) - + diff --git a/dist/ba_root/mods/tools/account.py b/dist/ba_root/mods/tools/account.py new file mode 100644 index 0000000..2ad2e7b --- /dev/null +++ b/dist/ba_root/mods/tools/account.py @@ -0,0 +1,80 @@ +# ba_meta require api 6 +from __future__ import annotations + +import ba +import bacommon.cloud +import logging +from efro.error import CommunicationError + + + +STATUS_CHECK_INTERVAL_SECONDS = 2.0 + +class AccountUtil: + def __init__(self): + self._proxyid: str | None = None + self._proxykey: str | None = None + ba.internal.sign_out_v1() + ba.app.cloud.send_message_cb(bacommon.cloud.LoginProxyRequestMessage(), + on_response=ba.Call(self._on_proxy_request_response)) + + def _on_proxy_request_response(self, response: bacommon.cloud.LoginProxyRequestResponse | Exception) -> None: + if isinstance(response, Exception): + logging.error("error occured") + logging.critical("Falling back to V1 account") + ba.internal.sign_in_v1('Local') + return + address = ba.internal.get_master_server_address( + version=2) + response.url + logging.debug("Copy this URL to your browser : " +address) + self._proxyid = response.proxyid + self._proxykey = response.proxykey + ba.timer(STATUS_CHECK_INTERVAL_SECONDS, + ba.Call(self._ask_for_status)) + + def _ask_for_status(self) -> None: + assert self._proxyid is not None + assert self._proxykey is not None + ba.app.cloud.send_message_cb( + bacommon.cloud.LoginProxyStateQueryMessage( + proxyid=self._proxyid, proxykey=self._proxykey), + on_response=ba.Call(self._got_status)) + + def _got_status( + self, response: bacommon.cloud.LoginProxyStateQueryResponse | Exception + ) -> None: + # For now, if anything goes wrong on the server-side, just abort + # with a vague error message. Can be more verbose later if need be. + if (isinstance(response, bacommon.cloud.LoginProxyStateQueryResponse) + and response.state is response.State.FAIL): + logging.error("error occured ..terminating login request") + logging.critical("Falling back to V1 account") + ba.internal.sign_in_v1('Local') + + # If we got a token, set ourself as signed in. Hooray! + if (isinstance(response, bacommon.cloud.LoginProxyStateQueryResponse) + and response.state is response.State.SUCCESS): + assert response.credentials is not None + ba.app.accounts_v2.set_primary_credentials(response.credentials) + + ba.timer(3,self._logged_in) + + return + + # If we're still waiting, ask again soon. + if (isinstance(response, Exception) + or response.state is response.State.WAITING): + ba.timer(STATUS_CHECK_INTERVAL_SECONDS, + ba.Call(self._ask_for_status)) + def _logged_in(self): + logging.info("Logged in as: "+ba.internal.get_v1_account_display_string()) + +# #ba_meta export plugin +# class AccountV2(ba.Plugin): +# def __init__(self): +# if(ba.internal.get_v1_account_state()=='signed_in' and ba.internal.get_v1_account_type()=='V2'): +# logging.debug("Account V2 is active") +# else: +# logging.warning("Account V2 login require ....stay tuned.") +# ba.timer(3, ba.Call(logging.debug,"Starting Account V2 login process....")) +# ba.timer(6,AccountUtil) diff --git a/dist/ba_root/mods/tools/playlist.py b/dist/ba_root/mods/tools/playlist.py index 5dd40d1..5e668a4 100644 --- a/dist/ba_root/mods/tools/playlist.py +++ b/dist/ba_root/mods/tools/playlist.py @@ -4,6 +4,7 @@ import ba import _ba +import ba.internal #session change by smoothy from ba._freeforallsession import FreeForAllSession from ba._dualteamsession import DualTeamSession @@ -24,7 +25,7 @@ def set_playlist(content): _playlists_var = "{} Playlists".format(content["playlistType"]) playlists = _ba.app.config[_playlists_var] playlist = playlists[content["playlistName"]] - _ba.chatmessage("Fetched playlist:"+content["playlistName"]) + ba.internal.chatmessage("Fetched playlist:"+content["playlistName"]) typename = ( 'teams' if content['playlistType'] == 'Team Tournament' else 'ffa' if content['playlistType'] == 'Free-for-All' else '??') @@ -32,14 +33,13 @@ def set_playlist(content): def set_playlist_inline(playlist,newPLaylistType): - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() if (isinstance(session,DualTeamSession) or isinstance(session,CoopSession)) and newPLaylistType=='ffa': - #_ba.get_foreground_host_activity().end_game() - _ba.get_foreground_host_session().end() + ba.internal.get_foreground_host_session().end() _thread.start_new_thread(withDelay,(FreeForAllSession,playlist,)) elif (isinstance(session,FreeForAllSession) or isinstance(session,CoopSession))and newPLaylistType=="teams": - _ba.get_foreground_host_session().end() + ba.internal.get_foreground_host_session().end() _thread.start_new_thread(withDelay,(DualTeamSession,playlist,)) else: updatePlaylist(playlist) @@ -51,14 +51,14 @@ def withDelay(session,playlist): _ba.pushcall(Call(updateSession,session,playlist),from_other_thread=True) def updateSession(session,playlist): - _ba.new_host_session(session) + ba.internal.new_host_session(session) if playlist: updatePlaylist(playlist) def updatePlaylist(playlist): - session = _ba.get_foreground_host_session() + session = ba.internal.get_foreground_host_session() content = ba._playlist.filter_playlist( playlist, sessiontype=type(session), @@ -75,14 +75,14 @@ def set_next_map(session, game_map): def playlist(code): - _ba.add_transaction( + ba.internal.add_transaction( { 'type': 'IMPORT_PLAYLIST', 'code': str(code), 'overwrite': True }, callback=set_playlist) - _ba.run_transactions() + ba.internal.run_transactions() @@ -96,26 +96,26 @@ def setPlaylist(para): elif para in settings["playlists"]: playlist(settings["playlists"][para]) else: - _ba.chatmessage("Available Playlist") + ba.internal.chatmessage("Available Playlist") for play in settings["playlists"]: - _ba.chatmessage(play) + ba.internal.chatmessage(play) def flush_playlists(): print("Clearing old playlists..") for playlist in _ba.app.config["Team Tournament Playlists"]: - _ba.add_transaction( + ba.internal.add_transaction( { "type":"REMOVE_PLAYLIST", "playlistType":"Team Tournament", "playlistName":playlist }) - _ba.run_transactions() + ba.internal.run_transactions() for playlist in _ba.app.config["Free-for-All Playlists"]: - _ba.add_transaction( + ba.internal.add_transaction( { "type":"REMOVE_PLAYLIST", "playlistType":"Free-for-All", "playlistName":playlist }) - _ba.run_transactions() + ba.internal.run_transactions() diff --git a/dist/ba_root/mods/tools/servercheck.py b/dist/ba_root/mods/tools/servercheck.py index 5832c02..516f752 100644 --- a/dist/ba_root/mods/tools/servercheck.py +++ b/dist/ba_root/mods/tools/servercheck.py @@ -6,6 +6,7 @@ from serverData import serverdata from playersData import pdata import _ba +import ba.internal import urllib.request import json import datetime @@ -31,7 +32,7 @@ class checkserver(object): def check(self): newPlayers = [] - for ros in _ba.get_game_roster(): + for ros in ba.internal.get_game_roster(): newPlayers.append(ros['account_id']) if ros['account_id'] not in self.players and ros[ @@ -55,7 +56,7 @@ class checkserver(object): "sys") except: pass - _ba.disconnect_client(ros['client_id'], 1) + ba.internal.disconnect_client(ros['client_id'], 1) return if settings["whitelist"] and ros["account_id"] != None: @@ -65,7 +66,7 @@ class checkserver(object): clients=[ros['client_id']]) logger.log(d_str + "||" + ros[ "account_id"] + " | kicked > not in whitelist") - _ba.disconnect_client(ros['client_id']) + ba.internal.disconnect_client(ros['client_id']) return @@ -84,7 +85,7 @@ def on_player_join_server(pbid, player_data): now = time.time() # player_data=pdata.get_info(pbid) clid = 113 - for ros in _ba.get_game_roster(): + for ros in ba.internal.get_game_roster(): if ros["account_id"] == pbid: clid = ros["client_id"] if pbid in serverdata.clients: @@ -97,7 +98,7 @@ def on_player_join_server(pbid, player_data): color=(1, 0, 1), transient=True, clients=[clid]) logger.log(pbid + "|| kicked for joining too fast") - _ba.disconnect_client(clid) + ba.internal.disconnect_client(clid) _thread.start_new_thread(reportSpam, (pbid,)) @@ -112,7 +113,7 @@ def on_player_join_server(pbid, player_data): device_strin = "" if player_data["isBan"] or get_account_age(player_data["accountAge"]) < \ settings["minAgeToJoinInHours"]: - for ros in _ba.get_game_roster(): + for ros in ba.internal.get_game_roster(): if ros['account_id'] == pbid: if not player_data["isBan"]: _ba.screenmessage( @@ -120,7 +121,7 @@ def on_player_join_server(pbid, player_data): color=(1, 0, 0), transient=True, clients=[ros['client_id']]) logger.log(pbid + " | kicked > reason:Banned account") - _ba.disconnect_client(ros['client_id']) + ba.internal.disconnect_client(ros['client_id']) return else: @@ -137,7 +138,7 @@ def on_player_join_server(pbid, player_data): verify_account(pbid, player_data) cid = 113 d_st = "xx" - for ros in _ba.get_game_roster(): + for ros in ba.internal.get_game_roster(): if ros['account_id'] == pbid: cid = ros['client_id'] d_st = ros['display_string'] @@ -156,7 +157,7 @@ def on_player_join_server(pbid, player_data): d_string = "" cid = 113 - for ros in _ba.get_game_roster(): + for ros in ba.internal.get_game_roster(): if ros['account_id'] == pbid: d_string = ros['display_string'] cid = ros['client_id'] @@ -177,7 +178,7 @@ def on_player_join_server(pbid, player_data): def verify_account(pb_id, p_data): d_string = "" - for ros in _ba.get_game_roster(): + for ros in ba.internal.get_game_roster(): if ros['account_id'] == pb_id: d_string = ros['display_string'] @@ -309,10 +310,10 @@ def save_ids(ids, pb_id, display_string): def kick_by_pb_id(pb_id, msg): - for ros in _ba.get_game_roster(): + for ros in ba.internal.get_game_roster(): if ros['account_id'] == pb_id: _ba.screenmessage(msg, transient=True, clients=[ros['client_id']]) - _ba.disconnect_client(ros['client_id']) + ba.internal.disconnect_client(ros['client_id']) def get_account_age(ct): diff --git a/dist/bombsquad_headless b/dist/bombsquad_headless index bc10d68..a2c1161 100644 Binary files a/dist/bombsquad_headless and b/dist/bombsquad_headless differ