Merge pull request #65 from imayushsaini/1.7

1.7.10
This commit is contained in:
Ayush Saini 2022-10-02 21:46:10 +05:30 committed by GitHub
commit e53eec6f2b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
194 changed files with 7534 additions and 2166 deletions

View file

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

901
ballisticacore_server Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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": "<Create/Edit Player>",
@ -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": {

View file

@ -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": "<pumipili ng manlalaro>",
"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": "<Gumawa/I-Edit Ng Manlalaro>",
@ -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 ibat 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 ibat 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.",

View file

@ -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' dinterfé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",

View file

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

View file

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

View file

@ -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": "Καλύτεροι Χρόνοι Παγκοσμίως",

View file

@ -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": "विश्व का सबसे अधिक समय",

View file

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

View file

@ -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": "<Buat/Edit pemain>",
@ -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",

View file

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

View file

@ -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": "세계 최고 시간",

View file

@ -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": "از آموزش می گذریم",

View file

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

View file

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

View file

@ -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": "пропуск обучения...",

View file

@ -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": "<Crear/Editar Jugador>",
"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"
},

View file

@ -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 வயர்லெஸ் கட்டுப்படுத்திகளை இணைக்க உங்களை அனுமதிக்கிறது.",

View file

@ -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": "เวลาที่ดีที่สุดในโลก",

View file

@ -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": "<Oyuncu Oluştur/Düzenle>",
@ -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": {

View file

@ -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": "Кращий світовий час",

View file

@ -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 sol 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 sol 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 nantro\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": "<Crea/Muda zugador>",
@ -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 del 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}:"
},

View file

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

View file

@ -1,4 +1,4 @@
from .core import contents, where
__all__ = ["contents", "where"]
__version__ = "2022.06.15"
__version__ = "2022.09.14"

View file

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

View file

@ -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:
def contents() -> str:
return read_text("certifi", "cacert.pem", encoding="ascii")

241
dist/ba_data/python/_bainternal.py vendored Normal file
View file

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

View file

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

View file

@ -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,7 +109,7 @@ class AccountV1Subsystem:
if data['p']:
pro_mult = 1.0 + float(
_ba.get_v1_account_misc_read_val('proPowerRankingBoost',
_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()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

175
dist/ba_data/python/ba/_bootstrap.py vendored Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

367
dist/ba_data/python/ba/_internal.py vendored Normal file
View file

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

View file

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

View file

@ -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:
def _run_scan_in_bg(self) -> None:
"""Runs a scan (for use in background thread)."""
try:
cls = getclass(gameclassname, GameActivity)
gameclasses.append(cls)
except Exception:
from ba import _error
_error.print_exception('error importing ' + str(gameclassname))
unowned = self.get_unowned_game_types()
return [cls for cls in gameclasses if cls not in unowned]
def get_unowned_game_types(self) -> set[type[ba.GameActivity]]:
"""Return present game types not owned by the current account."""
try:
from ba import _store
unowned_games: set[type[ba.GameActivity]] = set()
if not _ba.app.headless_mode:
for section in _store.get_store_layout()['minigames']:
for mname in section['items']:
if not _ba.get_purchased(mname):
m_info = _store.get_store_item(mname)
unowned_games.add(m_info['gametype'])
return unowned_games
except Exception:
from ba import _error
_error.print_exception('error calcing un-owned games')
return set()
class ScanThread(threading.Thread):
"""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 <NUM>" lines found;'
' ignoring module.\n')
self.results.warnings.append(
f'Warning: {subpath}: multiple'
' "# ba_meta require api <NUM>" 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 <NUM>" line found;'
' ignoring module.\n')
self.results.warnings.append(
f'Warning: {subpath}:'
' no valid "# ba_meta require api <NUM>" line found;'
' ignoring module.')
return None

View file

@ -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,
playlist_resolved = _playlist.filter_playlist(
playlist,
sessiontype=type(self),
add_resolved_type=True)
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.')

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,101 +42,20 @@ 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
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,
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
@ -158,8 +66,7 @@ class CoopJoinActivity(JoinActivity):
if not (ba.app.demo_mode or ba.app.arcade_mode):
achievements = [
a
for a in ba.app.ach.achievements_for_coop_level(levelname)
a for a in ba.app.ach.achievements_for_coop_level(levelname)
if not a.complete
]
have_achievements = bool(achievements)

View file

@ -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',
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,7 +743,8 @@ 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,
ba.internal.submit_score(
self._game_name_str,
self._game_config_str,
name_str,
self._score,
@ -757,7 +758,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
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,8 +1075,11 @@ 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() + '/' +
# 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:

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,10 +155,11 @@ 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(
show_linked = (self._signed_in
and ba.internal.get_v1_account_misc_read_val(
'allowAccountLinking2', False))
if (account_state_num != self._account_state_num
@ -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')

View file

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

View file

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

View file

@ -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,7 +91,8 @@ 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(
if (is_browser_likely_available()
and ba.internal.get_v1_account_misc_read_val(
'showAccountExtrasMenu', False)):
self._extras_menu_button = ba.buttonwidget(
@ -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.

View file

@ -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,16 +128,16 @@ 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]),
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:
@ -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()

View file

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

View file

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

View file

@ -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,
ba.internal.fade_screen(
False,
time=0.2,
endcall=lambda: ba.quit(soft=True, back=self._back))
_ba.lock_all_input()
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)

View file

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

View file

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

View file

@ -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',
and not ba.internal.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.easter_egg_hunt'))):
unlocked = False
# Let's tint levels a slightly different color when easy mode

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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.'
text=ba.Lstr(
resource='gatherWindow.'
'manualRouterForwardingText',
subs=[('${PORT}',
str(_ba.get_game_port()))]),
subs=[
('${PORT}', str(ba.internal.get_game_port())),
],
),
color=color_bad,
)

View file

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

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