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 # 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 ## Requirements
- Ubuntu 20 - Ubuntu 20 and above
- python3.10 - python3.10
## Getting Started ## Getting Started
@ -33,6 +33,7 @@ Here you can ban player , mute them , disable their kick votes
## Features ## Features
- Rank System. - Rank System.
- Chat commands. - 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. - 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. - Rejoin cooldown.
- Leaderboard , top 3 rank players name on top right corner. - 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. - Integrated ElPatronPowerups.
- Auto switch to coop mode when players are less then threshold. - 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 - 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 Splitted Team in game score screen.
- New final score screen , StumbledScoreScreen. - New final score screen , StumbledScoreScreen.
- other small small feature improvement here there find yourself. - 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", "Abinav",
"Abir", "Abir",
"Abolfadl", "Abolfadl",
"Abolfazl",
"Abraham", "Abraham",
"Roman Abramov", "Roman Abramov",
"AC", "AC",
"Achref",
"adan", "adan",
"Adeel (AdeZ {@adez_})", "Adeel (AdeZ {@adez_})",
"Adel", "Adel",
@ -287,7 +289,9 @@
"bob bobber", "bob bobber",
"The Bomboler 💣", "The Bomboler 💣",
"bombsquad", "bombsquad",
"Bombsquadzueira",
"Bomby", "Bomby",
"Zeleni bomby",
"Book", "Book",
"Lucas Borges", "Lucas Borges",
"Gianfranco Del Borrello", "Gianfranco Del Borrello",
@ -337,6 +341,7 @@
"Fabio Cannavacciuolo", "Fabio Cannavacciuolo",
"CANOVER", "CANOVER",
"Fedrigo Canpanjoło", "Fedrigo Canpanjoło",
"CarlosE.",
"mark Dave a carposo", "mark Dave a carposo",
"Fabricio de Carvalho", "Fabricio de Carvalho",
"Joshua Castañeda", "Joshua Castañeda",
@ -370,6 +375,7 @@
"David Cot", "David Cot",
"Nayib Méndez Coto", "Nayib Méndez Coto",
"Dylan cotten", "Dylan cotten",
"covcheg",
"COVER", "COVER",
"crac", "crac",
"CrazyBear", "CrazyBear",
@ -424,6 +430,7 @@
"Diase7en", "Diase7en",
"ferbie Dicen", "ferbie Dicen",
"Diego788", "Diego788",
"DiegoGD",
"DiGGDaGG", "DiGGDaGG",
"dikivan2000", "dikivan2000",
"Dimitriy", "Dimitriy",
@ -437,6 +444,8 @@
"dlw", "dlw",
"DMarci", "DMarci",
"Dmirus", "Dmirus",
"Dmitriy",
"Savchenko Dmitriy",
"Count Su Doku", "Count Su Doku",
"DominikSikora!", "DominikSikora!",
"Kai Dominique", "Kai Dominique",
@ -482,6 +491,7 @@
"EnderDust123", "EnderDust123",
"EnderKay", "EnderKay",
"EnglandFirst", "EnglandFirst",
"Enrico",
"enzo", "enzo",
"Erick", "Erick",
"Erkam", "Erkam",
@ -511,6 +521,7 @@
"Syed Irfan Farhan", "Syed Irfan Farhan",
"Luiz Henrique Faria", "Luiz Henrique Faria",
"Syed Fahrin Farihin", "Syed Fahrin Farihin",
"Fatih",
"FaultyAdventure", "FaultyAdventure",
"Putra Riski Fauzi", "Putra Riski Fauzi",
"fauziozan.23@gmail.com", "fauziozan.23@gmail.com",
@ -558,6 +569,7 @@
"Gabriele", "Gabriele",
"Nihar Gajare", "Nihar Gajare",
"GalaxyNinja2003", "GalaxyNinja2003",
"AP - Pro Gamer",
"Proff Gamer", "Proff Gamer",
"Eduardo Gamer05", "Eduardo Gamer05",
"Taufiq Gamera", "Taufiq Gamera",
@ -583,6 +595,7 @@
"Giovalli99", "Giovalli99",
"Giovanny", "Giovanny",
"Dc superhero girl", "Dc superhero girl",
"Givij",
"Glu10free", "Glu10free",
"Mr. Glu10free", "Mr. Glu10free",
"Jhon Zion N. delos reyes gmail", "Jhon Zion N. delos reyes gmail",
@ -694,10 +707,11 @@
"Anestis Ioakimidis", "Anestis Ioakimidis",
"Dragomir Ioan", "Dragomir Ioan",
"Isa", "Isa",
"Israelme03",
"Tobias Dencker Israelsen", "Tobias Dencker Israelsen",
"Kegyes István", "Kegyes István",
"Itamar", "Itamar",
"Ivan", "ivan",
"iViietZ", "iViietZ",
"JaaJ", "JaaJ",
"Al jabbar", "Al jabbar",
@ -752,6 +766,7 @@
"Jules", "Jules",
"juse", "juse",
"Justine", "Justine",
"JYLE",
"Jyothish", "Jyothish",
"Oliver Jõgar", "Oliver Jõgar",
"Nackter Jörg", "Nackter Jörg",
@ -866,6 +881,7 @@
"Linux44313", "Linux44313",
"LiteBalt", "LiteBalt",
"LittleNyanCat", "LittleNyanCat",
"Juunhao Liu",
"Lizz", "Lizz",
"Lizzetc", "Lizzetc",
"Lkham", "Lkham",
@ -879,6 +895,7 @@
"lorenzo", "lorenzo",
"Lostguybrazil", "Lostguybrazil",
"mian louw", "mian louw",
"69 lover",
"Jordan Vega Loza", "Jordan Vega Loza",
"Chenging Lu", "Chenging Lu",
"Chengming Lu", "Chengming Lu",
@ -889,6 +906,7 @@
"Ludovico", "Ludovico",
"Luis (GalaxyM4)", "Luis (GalaxyM4)",
"Jose Luis", "Jose Luis",
"Luis(GalaxtM4)",
"luislinares", "luislinares",
"luispro25", "luispro25",
"Luka", "Luka",
@ -948,6 +966,8 @@
"Matteo", "Matteo",
"Matthias", "Matthias",
"Ihsan Maulana ( @ihsanm27)", "Ihsan Maulana ( @ihsanm27)",
"Muhammad Akbar Maulana",
"Mavook",
"Federico Mazzone", "Federico Mazzone",
"Andrea Mazzucchelli", "Andrea Mazzucchelli",
"Medic", "Medic",
@ -982,6 +1002,7 @@
"Mk", "Mk",
"MKG", "MKG",
"mobin", "mobin",
"Mobina",
"Moh", "Moh",
"Mohamadali", "Mohamadali",
"Mohamadamin", "Mohamadamin",
@ -1011,6 +1032,7 @@
"MrS0meone", "MrS0meone",
"Ivan Ms", "Ivan Ms",
"Msta", "Msta",
"MT",
"Muhammed Muhsin", "Muhammed Muhsin",
"MujtabaFR", "MujtabaFR",
"Muni", "Muni",
@ -1060,6 +1082,7 @@
"طارق محمد رضا سعيد NinjaStarXD", "طارق محمد رضا سعيد NinjaStarXD",
"nino", "nino",
"Nintendero65", "Nintendero65",
"Nizril",
"Nnubes256", "Nnubes256",
"Bu nny", "Bu nny",
"Noam", "Noam",
@ -1067,6 +1090,7 @@
"NofaseCZ", "NofaseCZ",
"Max Noisa", "Max Noisa",
"Noisb", "Noisb",
"None",
"Noobslaya101", "Noobslaya101",
"noorjandle1", "noorjandle1",
"Petter Nordlander", "Petter Nordlander",
@ -1076,10 +1100,12 @@
"Dhimas Wildan Nz", "Dhimas Wildan Nz",
"*** Adel NZ. ***", "*** Adel NZ. ***",
"Ognjen", "Ognjen",
"okko",
"Bastián Olea", "Bastián Olea",
"Nikita Oleshko", "Nikita Oleshko",
"Omar", "Omar",
"On3GaMs", "On3GaMs",
"No one",
"Adam Oros", "Adam Oros",
"Andrés Ortega", "Andrés Ortega",
"Zangar Orynbetov", "Zangar Orynbetov",
@ -1091,6 +1117,7 @@
"Giorgio Palmieri", "Giorgio Palmieri",
"Abhinay Pandey", "Abhinay Pandey",
"PangpondTH", "PangpondTH",
"PanKonKezo",
"PantheRoP", "PantheRoP",
"ParadoxPlayz", "ParadoxPlayz",
"Gavin Park", "Gavin Park",
@ -1109,9 +1136,11 @@
"pc192089", "pc192089",
"PC261133", "PC261133",
"PC295933", "PC295933",
"PC432736",
"pebikristia", "pebikristia",
"Pedro", "Pedro",
"Jiren/Juan Pedro", "Jiren/Juan Pedro",
"Penta :D",
"Peque", "Peque",
"Rode Liliana Miranda Pereira", "Rode Liliana Miranda Pereira",
"Jura Perić", "Jura Perić",
@ -1147,6 +1176,7 @@
"Pong", "Pong",
"Pooya", "Pooya",
"pouriya", "pouriya",
"Pouya",
"Pranav", "Pranav",
"Luca Preibsch", "Luca Preibsch",
"Prem", "Prem",
@ -1172,6 +1202,7 @@
"raghul", "raghul",
"khaled rahma", "khaled rahma",
"Rayhan Rahmats", "Rayhan Rahmats",
"Ralfreengz",
"1. Ramagister", "1. Ramagister",
"Rostislav RAMAGISTER", "Rostislav RAMAGISTER",
"Ростислав RAMAGISTER", "Ростислав RAMAGISTER",
@ -1276,6 +1307,7 @@
"Jhon Rodel Sayo", "Jhon Rodel Sayo",
"Christiaan Schriel", "Christiaan Schriel",
"Hendrik Schur", "Hendrik Schur",
"SEBASTIAN2059",
"Semen", "Semen",
"Mihai Serbanica", "Mihai Serbanica",
"Daniel Balam Cabrera Serrano", "Daniel Balam Cabrera Serrano",
@ -1312,6 +1344,7 @@
"sobhan", "sobhan",
"Nikhil sohan", "Nikhil sohan",
"SoK", "SoK",
"SoldierBS",
"SPT Sosat", "SPT Sosat",
"Soto", "Soto",
"SpacingBat3", "SpacingBat3",
@ -1377,12 +1410,14 @@
"TempVolcano3200", "TempVolcano3200",
"Yan Teryokhin", "Yan Teryokhin",
"TestGame1", "TestGame1",
"TestGame1👽🔥",
"testwindows8189", "testwindows8189",
"tgd4", "tgd4",
"Than", "Than",
"Thanakorn7215", "Thanakorn7215",
"thatFlaviooo", "thatFlaviooo",
"The_Blinded", "The_Blinded",
"Eugene (a.k.a TheBomber3000)",
"Thebosslol66", "Thebosslol66",
"thejoker190101", "thejoker190101",
"TheLLage", "TheLLage",
@ -1426,6 +1461,7 @@
"Uros", "Uros",
"clarins usap", "clarins usap",
"Uzinerz", "Uzinerz",
"Shohrux V",
"Vader", "Vader",
"Valentin", "Valentin",
"Valkan1975", "Valkan1975",
@ -1463,6 +1499,7 @@
"webparham", "webparham",
"Wesley", "Wesley",
"whitipet", "whitipet",
"wibi9424",
"Wido2000", "Wido2000",
"wildanae", "wildanae",
"Will", "Will",
@ -1482,6 +1519,7 @@
"Francisco Xavier", "Francisco Xavier",
"xbarix123897", "xbarix123897",
"Peque XD", "Peque XD",
"Xem",
"Xizruh", "Xizruh",
"xxonx8", "xxonx8",
"Ajeet yadav", "Ajeet yadav",
@ -1496,6 +1534,7 @@
"Kenneth Yoneyama", "Kenneth Yoneyama",
"yossef", "yossef",
"youcef", "youcef",
"Youssef",
"Yousuf", "Yousuf",
"Yovan182Sunbreaker", "Yovan182Sunbreaker",
"Yrtking", "Yrtking",
@ -1503,6 +1542,7 @@
"Yudhis", "Yudhis",
"yugo", "yugo",
"yullian", "yullian",
"Yuslendo",
"NEEROOA Muhammad Yusuf", "NEEROOA Muhammad Yusuf",
"Yuuki", "Yuuki",
"Yy", "Yy",
@ -1527,6 +1567,7 @@
"Riven Zhao", "Riven Zhao",
"jim ZHOU", "jim ZHOU",
"Mohammad ziar", "Mohammad ziar",
"ZioFesteeeer",
"zJairO", "zJairO",
"ZkyweR", "ZkyweR",
"Nagy Zoltán", "Nagy Zoltán",
@ -1599,6 +1640,8 @@
"محمد خالد", "محمد خالد",
"امیرحسین دهقان", "امیرحسین دهقان",
"امید رضازاده", "امید رضازاده",
"فاطمه عباس زاده ۸۴",
"فاطمه عباس زاده۸۴",
"محمد وائل سلطان", "محمد وائل سلطان",
"ص", "ص",
"عبداللہ صائم", "عبداللہ صائم",
@ -1657,9 +1700,11 @@
"神仙", "神仙",
"药药Medic", "药药Medic",
"蔚蓝枫叶", "蔚蓝枫叶",
"陈星宇你就是歌姬吧",
"鲨鱼服·Medic", "鲨鱼服·Medic",
"鲲鹏元帅", "鲲鹏元帅",
"꧁ephyro꧂", "꧁ephyro꧂",
"가라사대",
"공팔이", "공팔이",
"권찬근", "권찬근",
"김원재", "김원재",
@ -1673,6 +1718,8 @@
"이지민", "이지민",
"일베저장소", "일베저장소",
"전감호", "전감호",
"BombsquadKorea 네이버 카페" "BombsquadKorea 네이버 카페",
"Zona-BombSquad",
"CrazySquad"
] ]
} }

View file

@ -499,6 +499,7 @@
"welcome2Text": "يمكنك أيضا الحصول على تذاكر من العديد من الأنشطة نفسها.\nتذاكر يمكن استخدامها لفتح شخصيات جديدة، والخرائط، و\nالألعاب المصغرة، للدخول البطولات، وأكثر من ذلك.", "welcome2Text": "يمكنك أيضا الحصول على تذاكر من العديد من الأنشطة نفسها.\nتذاكر يمكن استخدامها لفتح شخصيات جديدة، والخرائط، و\nالألعاب المصغرة، للدخول البطولات، وأكثر من ذلك.",
"yourPowerRankingText": "تصنيف الطاقة:" "yourPowerRankingText": "تصنيف الطاقة:"
}, },
"copyConfirmText": "نسخ إلى اللوحة",
"copyOfText": "${NAME} نسخ", "copyOfText": "${NAME} نسخ",
"copyText": "ينسخ", "copyText": "ينسخ",
"createEditPlayerText": "<اصنع او عدل حساب>", "createEditPlayerText": "<اصنع او عدل حساب>",
@ -626,7 +627,7 @@
"epicDescriptionFilterText": "${DESCRIPTION} بحركة ملحمية بطيئة", "epicDescriptionFilterText": "${DESCRIPTION} بحركة ملحمية بطيئة",
"epicNameFilterText": "الملحمي ${NAME}", "epicNameFilterText": "الملحمي ${NAME}",
"errorAccessDeniedText": "تم الرفض", "errorAccessDeniedText": "تم الرفض",
"errorDeviceTimeIncorrectText": "توقف وقت جهازك بمقدار ${HOURS} ساعة.\nهذا قد يتسبب بمشاكل.\nمن فضلك قم بالتحقق من اعدادات الوقت.", "errorDeviceTimeIncorrectText": ".من الساعات ${HOURS} وقت جهازك غير صحيح بمقدار\n.هذا سوف يتسبب بمشاكل\nمن فضلك قم بالتحقق من اعدادات الوقت.",
"errorOutOfDiskSpaceText": "انتهت مساحة التخزين", "errorOutOfDiskSpaceText": "انتهت مساحة التخزين",
"errorSecureConnectionFailText": "تعذر انشاء اتصال سحابي أمن; قد تفشل وظائف الشبكة.", "errorSecureConnectionFailText": "تعذر انشاء اتصال سحابي أمن; قد تفشل وظائف الشبكة.",
"errorText": "خطا", "errorText": "خطا",
@ -1367,6 +1368,7 @@
"tournamentStandingsText": "ترتيب البطولة", "tournamentStandingsText": "ترتيب البطولة",
"tournamentText": "المسابقة", "tournamentText": "المسابقة",
"tournamentTimeExpiredText": "انتهت مدة البطولة", "tournamentTimeExpiredText": "انتهت مدة البطولة",
"tournamentsDisabledWorkspaceText": "البطولات لا تعمل عندما تكون فضائات العمل تعمل.\nلتشغيل البطولات مجددا، قم بإلغاء تشغيل فضاء العمل الخاص بك و اعادة تشغيل اللعبة.",
"tournamentsText": "البطولات", "tournamentsText": "البطولات",
"translations": { "translations": {
"characterNames": { "characterNames": {

View file

@ -501,6 +501,7 @@
"welcome2Text": "你还可参加很多相同活动来赢取点券。\n点券可用于解锁新的角色、地图和\n迷你游戏或进入锦标赛或更多用途", "welcome2Text": "你还可参加很多相同活动来赢取点券。\n点券可用于解锁新的角色、地图和\n迷你游戏或进入锦标赛或更多用途",
"yourPowerRankingText": "你的能力排位:" "yourPowerRankingText": "你的能力排位:"
}, },
"copyConfirmText": "复制到剪贴板",
"copyOfText": "${NAME} 复制", "copyOfText": "${NAME} 复制",
"copyText": "copy", "copyText": "copy",
"createEditPlayerText": "<创建/编辑玩家>", "createEditPlayerText": "<创建/编辑玩家>",
@ -630,7 +631,7 @@
"epicDescriptionFilterText": "史诗级慢动作 ${DESCRIPTION}。", "epicDescriptionFilterText": "史诗级慢动作 ${DESCRIPTION}。",
"epicNameFilterText": "史诗级${NAME}", "epicNameFilterText": "史诗级${NAME}",
"errorAccessDeniedText": "访问被拒绝", "errorAccessDeniedText": "访问被拒绝",
"errorDeviceTimeIncorrectText": "您的系统时间与服务器时间相差了${HOURS}小时\n这可能会出现一些问题\n请检查您的系统时间或时区", "errorDeviceTimeIncorrectText": "您设备的时间有 ${HOURS} 小时的误差。\n这会导致游戏出现问题。\n请检查您设备的时间和时区设置。",
"errorOutOfDiskSpaceText": "磁盘空间不足", "errorOutOfDiskSpaceText": "磁盘空间不足",
"errorSecureConnectionFailText": "无法建立安全的云链接,网络可能会连接失败", "errorSecureConnectionFailText": "无法建立安全的云链接,网络可能会连接失败",
"errorText": "错误", "errorText": "错误",
@ -1385,6 +1386,7 @@
"tournamentStandingsText": "锦标赛积分榜", "tournamentStandingsText": "锦标赛积分榜",
"tournamentText": "锦标赛", "tournamentText": "锦标赛",
"tournamentTimeExpiredText": "锦标赛时间结束", "tournamentTimeExpiredText": "锦标赛时间结束",
"tournamentsDisabledWorkspaceText": "工作区启用时无法参加锦标赛!\n关闭工作区才能进入锦标赛。",
"tournamentsText": "锦标赛", "tournamentsText": "锦标赛",
"translations": { "translations": {
"characterNames": { "characterNames": {

View file

@ -787,7 +787,7 @@
"ticketPack4Text": "局型點券包", "ticketPack4Text": "局型點券包",
"ticketPack5Text": "巨巨巨巨巨巨巨巨巨巨型點券包", "ticketPack5Text": "巨巨巨巨巨巨巨巨巨巨型點券包",
"ticketPack6Text": "終極點券包", "ticketPack6Text": "終極點券包",
"ticketsFromASponsorText": "從贊助商\n獲取${COUNT}點券", "ticketsFromASponsorText": "看推廣影片\n獲取${COUNT}點券",
"ticketsText": "${COUNT} 點券", "ticketsText": "${COUNT} 點券",
"titleText": "獲得點券", "titleText": "獲得點券",
"unavailableLinkAccountText": "對不起,該平台不可進行購買\n您可以將賬戶鏈接到另一個\n平台以進行購買", "unavailableLinkAccountText": "對不起,該平台不可進行購買\n您可以將賬戶鏈接到另一個\n平台以進行購買",
@ -798,6 +798,7 @@
"youHaveText": "你擁有 ${COUNT}點券" "youHaveText": "你擁有 ${COUNT}點券"
}, },
"googleMultiplayerDiscontinuedText": "抱歉Google的多人遊戲服務不再可用。\n我將盡快更換新的替代服務。\n在此之前請嘗試其他連接方法。\n-Eric", "googleMultiplayerDiscontinuedText": "抱歉Google的多人遊戲服務不再可用。\n我將盡快更換新的替代服務。\n在此之前請嘗試其他連接方法。\n-Eric",
"googlePlayPurchasesNotAvailableText": "Google Play購買不可用\n你可能需要更新你的Google Play商店組件",
"googlePlayText": "Google Play", "googlePlayText": "Google Play",
"graphicsSettingsWindow": { "graphicsSettingsWindow": {
"alwaysText": "總是", "alwaysText": "總是",
@ -1362,6 +1363,7 @@
"tournamentStandingsText": "錦標賽積分榜", "tournamentStandingsText": "錦標賽積分榜",
"tournamentText": "錦標賽", "tournamentText": "錦標賽",
"tournamentTimeExpiredText": "錦標賽時間結束", "tournamentTimeExpiredText": "錦標賽時間結束",
"tournamentsDisabledWorkspaceText": "儅工作區處於開啓狀態時講標賽將被禁用\n如果想解禁錦標賽請關閉您的工作區並重啓游戲",
"tournamentsText": "錦標賽", "tournamentsText": "錦標賽",
"translations": { "translations": {
"characterNames": { "characterNames": {

View file

@ -1,10 +1,10 @@
{ {
"accountSettingsWindow": { "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)", "accountProfileText": "(korisnički račun)",
"accountsText": "Profili", "accountsText": "Korisnički računi",
"achievementProgressText": "Postignuća: ${COUNT} od ${TOTAL}", "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.", "changeOncePerSeason": "Ovo možeš promjeniti samo jednom po sezoni.",
"changeOncePerSeasonError": "Moraš pričekati sljedeču sezonu da promjeniš ovo(${NUM} days)", "changeOncePerSeasonError": "Moraš pričekati sljedeču sezonu da promjeniš ovo(${NUM} days)",
"customName": "Prilagođeno ime", "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.", "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.", "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", "linkAccountsText": "Poveži profile",
"linkedAccountsText": "Povezani profili:", "linkedAccountsText": "Povezani računi:",
"nameChangeConfirm": "Promjeni svoje ime u ${NAME}?", "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?", "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?", "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ů", "ticketPack4Text": "Sloní Balíček Kupónů",
"ticketPack5Text": "Mamutí Balíček Kupónů!", "ticketPack5Text": "Mamutí Balíček Kupónů!",
"ticketPack6Text": "Ultimátní 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ů", "ticketsText": "${COUNT} Kupónů",
"titleText": "Získat Kupóny", "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.", "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ů" "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", "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", "googlePlayText": "Google Play",
"graphicsSettingsWindow": { "graphicsSettingsWindow": {
"alwaysText": "Vždy", "alwaysText": "Vždy",
@ -1385,6 +1386,7 @@
"tournamentStandingsText": "Pořadí v turnaji", "tournamentStandingsText": "Pořadí v turnaji",
"tournamentText": "Turnaj", "tournamentText": "Turnaj",
"tournamentTimeExpiredText": "Čas turnaje vypršel", "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", "tournamentsText": "Turnaje",
"translations": { "translations": {
"characterNames": { "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.", "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:" "yourPowerRankingText": "Your Power Ranking:"
}, },
"copyConfirmText": "Copied to clipboard.",
"copyOfText": "${NAME} Copy", "copyOfText": "${NAME} Copy",
"copyText": "Copy", "copyText": "Copy",
"createEditPlayerText": "<Create/Edit Player>", "createEditPlayerText": "<Create/Edit Player>",
@ -624,7 +625,7 @@
"epicDescriptionFilterText": "${DESCRIPTION} In epic slow motion.", "epicDescriptionFilterText": "${DESCRIPTION} In epic slow motion.",
"epicNameFilterText": "Epic ${NAME}", "epicNameFilterText": "Epic ${NAME}",
"errorAccessDeniedText": "access denied", "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", "errorOutOfDiskSpaceText": "out of disk space",
"errorSecureConnectionFailText": "Unable to establish secure cloud connection; network functionality may fail.", "errorSecureConnectionFailText": "Unable to establish secure cloud connection; network functionality may fail.",
"errorText": "Error", "errorText": "Error",
@ -1074,7 +1075,6 @@
"otherText": "Other...", "otherText": "Other...",
"outOfText": "(#${RANK} out of ${ALL})", "outOfText": "(#${RANK} out of ${ALL})",
"ownFlagAtYourBaseWarning": "Your own flag must be\nat your base to score!", "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": { "partyWindow": {
"chatMessageText": "Chat Message", "chatMessageText": "Chat Message",
"emptyText": "Your party is empty", "emptyText": "Your party is empty",
@ -1245,8 +1245,6 @@
"disableCameraGyroscopeMotionText": "Disable Camera Gyroscope Motion", "disableCameraGyroscopeMotionText": "Disable Camera Gyroscope Motion",
"disableCameraShakeText": "Disable Camera Shake", "disableCameraShakeText": "Disable Camera Shake",
"disableThisNotice": "(you can disable this notice in advanced settings)", "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", "enterPromoCodeText": "Enter Code",
"forTestingText": "Note: these values are only for testing and will be lost when the app exits.", "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!", "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", "tournamentStandingsText": "Tournament Standings",
"tournamentText": "Tournament", "tournamentText": "Tournament",
"tournamentTimeExpiredText": "Tournament Time Expired", "tournamentTimeExpiredText": "Tournament Time Expired",
"tournamentsDisabledWorkspaceText": "Tournaments are disabled when workspaces are active.\nTo re-enable tournaments, disable your workspace and restart.",
"tournamentsText": "Tournaments", "tournamentsText": "Tournaments",
"translations": { "translations": {
"characterNames": { "characterNames": {

View file

@ -176,7 +176,7 @@
"descriptionComplete": "Nanalo ng walang puntos ang kalaban", "descriptionComplete": "Nanalo ng walang puntos ang kalaban",
"descriptionFull": "Manalo sa ${LEVEL} ng walang puntos ang kalaban", "descriptionFull": "Manalo sa ${LEVEL} ng walang puntos ang kalaban",
"descriptionFullComplete": "Nanalo 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": { "Pro Football Victory": {
"description": "Panalunin ang laro", "description": "Panalunin ang laro",
@ -204,7 +204,7 @@
"descriptionComplete": "Nanalo nang hindi hinahayaang makapuntos ang kalaban", "descriptionComplete": "Nanalo nang hindi hinahayaang makapuntos ang kalaban",
"descriptionFull": "Manalo sa ${LEVEL} 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", "descriptionFullComplete": "Nanalo sa ${LEVEL} nang hindi hinahayaang makapuntos ang kalaban",
"name": "${LEVEL} Shutout" "name": "Pagsarhan ng ${LEVEL}"
}, },
"Rookie Football Victory": { "Rookie Football Victory": {
"description": "Ipanalo ang laro", "description": "Ipanalo ang laro",
@ -232,14 +232,14 @@
"descriptionComplete": "Nakapuntos ng 500", "descriptionComplete": "Nakapuntos ng 500",
"descriptionFull": "Pumuntos ng 500 sa ${LEVEL}", "descriptionFull": "Pumuntos ng 500 sa ${LEVEL}",
"descriptionFullComplete": "Nakakuha ng 500 puntos sa ${LEVEL}", "descriptionFullComplete": "Nakakuha ng 500 puntos sa ${LEVEL}",
"name": "${LEVEL} Master" "name": "Pinuno ng ${LEVEL}"
}, },
"Runaround Wizard": { "Runaround Wizard": {
"description": "Pumuntos ng 1000", "description": "Pumuntos ng 1000",
"descriptionComplete": "Naka score ng 1000 points", "descriptionComplete": "Naka score ng 1000 points",
"descriptionFull": "Mag score ng 1000 points sa ${LEVEL}", "descriptionFull": "Mag score ng 1000 points sa ${LEVEL}",
"descriptionFullComplete": "Naka score ng 1000 points sa ${LEVEL}", "descriptionFullComplete": "Naka score ng 1000 points sa ${LEVEL}",
"name": "${LEVEL} Wizard" "name": "Salamangkero ng ${LEVEL}"
}, },
"Sharing is Caring": { "Sharing is Caring": {
"descriptionFull": "I-share ang game sa iyong kaibigan", "descriptionFull": "I-share ang game sa iyong kaibigan",
@ -247,10 +247,10 @@
"name": "Ang pagbigay ay pag-alaga" "name": "Ang pagbigay ay pag-alaga"
}, },
"Stayin' Alive": { "Stayin' Alive": {
"description": "Manalo nang hindi namamatay", "description": "Manalo nang hindi namatay",
"descriptionComplete": "Nanalo nang hindi namatay", "descriptionComplete": "Nanalo nang hindi namatay",
"descriptionFull": "Nanalo ${LEVEL} 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" "name": "Manatiling Buhay"
}, },
"Super Mega Punch": { "Super Mega Punch": {
@ -298,7 +298,7 @@
"descriptionComplete": "Manalo nang hindi maka puntos ang mga kalaban", "descriptionComplete": "Manalo nang hindi maka puntos ang mga kalaban",
"descriptionFull": "Manalo sa ${LEVEL} na 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", "descriptionFullComplete": "Manalo sa ${LEVEL} na hindi maka puntos ang mga kalaban",
"name": "${LEVEL} Pagsarhan" "name": "Pagsarhan ng ${LEVEL}"
}, },
"Uber Football Victory": { "Uber Football Victory": {
"description": "Ipanalo ang laro", "description": "Ipanalo ang laro",
@ -357,10 +357,10 @@
"buttonText": "pindutan", "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.", "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", "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.", "challengeEndedText": "Natapos na ang challenge na ito.",
"chatMuteText": "I-mute ang Chat", "chatMuteText": "I-mute ang Chat",
"chatMutedText": "Chat Muted", "chatMutedText": "Na-mute ang Chat",
"chatUnMuteText": "I-unmute ang Chat", "chatUnMuteText": "I-unmute ang Chat",
"choosingPlayerText": "<pumipili ng manlalaro>", "choosingPlayerText": "<pumipili ng manlalaro>",
"completeThisLevelToProceedText": "I complete mo muna\nang level na ito bago ka mag-proceed!", "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.", "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:" "yourPowerRankingText": "Iyong Power Ranking:"
}, },
"copyConfirmText": "Nakopya sa clipboard.",
"copyOfText": "Kopya ng ${NAME}", "copyOfText": "Kopya ng ${NAME}",
"copyText": "I-kopya", "copyText": "I-kopya",
"createEditPlayerText": "<Gumawa/I-Edit Ng Manlalaro>", "createEditPlayerText": "<Gumawa/I-Edit Ng Manlalaro>",
@ -516,7 +517,7 @@
"specialThanksText": "Espesyal Na Pasasalamat:", "specialThanksText": "Espesyal Na Pasasalamat:",
"thanksEspeciallyToText": "Salamat, lalo na kay ${NAME}", "thanksEspeciallyToText": "Salamat, lalo na kay ${NAME}",
"titleText": "${APP_NAME} Mga Kredito", "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}", "currentStandingText": "Ang kasalukuyang tayo mo ay #${RANK}",
"customizeText": "I-customize...", "customizeText": "I-customize...",
@ -624,7 +625,7 @@
"epicDescriptionFilterText": "${DESCRIPTION} sa isang epic na slow motion.", "epicDescriptionFilterText": "${DESCRIPTION} sa isang epic na slow motion.",
"epicNameFilterText": "Epikong ${NAME}", "epicNameFilterText": "Epikong ${NAME}",
"errorAccessDeniedText": "walang pahintulot", "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", "errorOutOfDiskSpaceText": "Wala sa puwang ng disk",
"errorSecureConnectionFailText": "Hindi mapatunayan ng secure na “cloud connection”; maaaring mabigo ang pagpapagana ng network.", "errorSecureConnectionFailText": "Hindi mapatunayan ng secure na “cloud connection”; maaaring mabigo ang pagpapagana ng network.",
"errorText": "Error", "errorText": "Error",
@ -870,12 +871,12 @@
"hostIsNavigatingMenusText": "- Ang ${HOST} ay nagna-navigate sa mga menu tulad ng isang boss -", "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:", "importPlaylistCodeInstructionsText": "Gamitin ang sumusunod na code upang i-import ang playlist na ito sa ibang lugar:",
"importPlaylistSuccessText": "Na-import na ${TYPE} na playlist '${NAME}'", "importPlaylistSuccessText": "Na-import na ${TYPE} na playlist '${NAME}'",
"importText": "Iangkat", "importText": "I-Import",
"importingText": "Pag-Import…", "importingText": "Nag-Iimport…",
"inGameClippedNameText": "in-game ay naging\n\"${NAME}\"", "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.", "installDiskSpaceErrorText": "ERROR: Hindi makumpleto ang pag-install.\nMaaaring wala ka nang espasyo sa iyong device.\nMag-clear ng ilang espasyo at subukang muli.",
"internal": { "internal": {
"arrowsToExitListText": "pindutin ang ${LEFT} o ${RIGHT} upang lumabas sa listahan", "arrowsToExitListText": "pindutin ang ${LEFT} o ${RIGHT} upang mawala sa listahan",
"buttonText": "pindutan", "buttonText": "pindutan",
"cantKickHostError": "Hindi mo maaaring I-kick ang host.", "cantKickHostError": "Hindi mo maaaring I-kick ang host.",
"chatBlockedText": "Na-block si ${NAME} sa loob ng ${TIME} segundo.", "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…", "serverRestartingText": "Nagre-restart ang server. Mangyaring sumali muli sa isang saglit…",
"serverShuttingDownText": "Nagsasara ang server...", "serverShuttingDownText": "Nagsasara ang server...",
"signInErrorText": "Error sa pag-sign in.", "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.", "telnetAccessDeniedText": "ERROR: ang user ay hindi nagbigay ng access sa telnet.",
"timeOutText": "(time out sa ${TIME} segundo)", "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.", "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.", "keyboardChangeInstructionsText": "I-double press space para mapalitan ang mga keyboard.",
"keyboardNoOthersAvailableText": "Walang ibang mga keyboard na magagamit.", "keyboardNoOthersAvailableText": "Walang ibang mga keyboard na magagamit.",
"keyboardSwitchText": "Nagpapalit ng keyboard sa \"${NAME}\".", "keyboardSwitchText": "Nagpapalit ng keyboard sa \"${NAME}\".",
"kickOccurredText": "kicked na si ${NAME}", "kickOccurredText": "na-kicked si ${NAME}",
"kickQuestionText": "I-Kick si ${NAME}?", "kickQuestionText": "I-Kick si ${NAME}?",
"kickText": "", "kickText": "I-Kick",
"kickVoteCantKickAdminsText": "Hindi ma-kick ang mga admin.", "kickVoteCantKickAdminsText": "Hindi ma-kick ang mga admin.",
"kickVoteCantKickSelfText": "Hindi mo ma-kick ng sarili mo.", "kickVoteCantKickSelfText": "Hindi mo ma-kick ng sarili mo.",
"kickVoteFailedNotEnoughVotersText": "Hindi marami ang mga manlalaro para sa isang boto.", "kickVoteFailedNotEnoughVotersText": "Hindi marami ang mga manlalaro para sa isang boto.",
"kickVoteFailedText": "Nabigo ang kick-vote.", "kickVoteFailedText": "Nabigo ang kick-vote.",
"kickVoteStartedText": "Sinimulan na ang isang kick vote para kay ${NAME}.", "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.", "kickVotingDisabledText": "Naka-disable ang kick voting.",
"kickWithChatText": "I-type ang ${YES} sa chat para sa oo at ${NO} para sa hindi.", "kickWithChatText": "I-type ang ${YES} sa chat para sa oo at ${NO} para sa hindi.",
"killsTallyText": "${COUNT} na ang pumapatay", "killsTallyText": "${COUNT} ang pinatay",
"killsText": "Pumapatay", "killsText": "Pinatay",
"kioskWindow": { "kioskWindow": {
"easyText": "Madali", "easyText": "Madali",
"epicModeText": "Mode na Epic", "epicModeText": "Mode na Epic",
@ -962,22 +963,22 @@
"singlePlayerExamplesText": "Mga Halimbawa ng Single Player / Co-op", "singlePlayerExamplesText": "Mga Halimbawa ng Single Player / Co-op",
"versusExamplesText": "Halimbawa ng mga Versus" "versusExamplesText": "Halimbawa ng mga Versus"
}, },
"languageSetText": "Ang wika ay \"${LANGUAGE}\" na ngayon.", "languageSetText": "Ang wika ay \"${LANGUAGE}\" sa ngayon.",
"lapNumberText": "Ikot ${CURRENT}/${TOTAL}", "lapNumberText": "Ikot ${CURRENT}/${TOTAL}",
"lastGamesText": "(huling ${COUNT} na laro)", "lastGamesText": "(huling ${COUNT} na laro)",
"leaderboardsText": "Mga Leaderboard", "leaderboardsText": "Mga Leaderboard",
"league": { "league": {
"allTimeText": "Lahat Ng Oras", "allTimeText": "Lahat Ng Oras",
"currentSeasonText": "Kasalukuyang Season (${NUMBER})", "currentSeasonText": "Kasalukuyang Season (${NUMBER})",
"leagueFullText": "Liga ng ${NAME}.", "leagueFullText": "Ligang ${NAME}.",
"leagueRankText": "Ranggo ng Liga", "leagueRankText": "Ranggo ng Liga",
"leagueText": "Liga", "leagueText": "Liga",
"rankInLeagueText": "#${RANK}, ${NAME} ${SUFFIX} na 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.", "seasonEndsDaysText": "Matatapos ang season sa ${NUMBER} (na) araw.",
"seasonEndsHoursText": "Matatapos ang season sa ${NUMBER} (na) oras.", "seasonEndsHoursText": "Matatapos ang season sa ${NUMBER} (na) oras.",
"seasonEndsMinutesText": "Matatapos ang season sa ${NUMBER} (na) minuto.", "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.", "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." "trophyCountsResetText": "Ire-reset ang mga bilang ng tropeo sa susunod na season."
}, },
@ -988,7 +989,7 @@
"levelText": "Antas ${NUMBER}", "levelText": "Antas ${NUMBER}",
"levelUnlockedText": "Unlocked ang Level na Ito!", "levelUnlockedText": "Unlocked ang Level na Ito!",
"livesBonusText": "Bonus ng Buhay", "livesBonusText": "Bonus ng Buhay",
"loadingText": "Saglit lang...", "loadingText": "saglit lang...",
"loadingTryAgainText": "Naglo-load; subukan muli sa isang saglit…", "loadingTryAgainText": "Naglo-load; subukan muli sa isang saglit…",
"macControllerSubsystemBothText": "Pareho (hindi inirerekomenda)", "macControllerSubsystemBothText": "Pareho (hindi inirerekomenda)",
"macControllerSubsystemClassicText": "Klasiko", "macControllerSubsystemClassicText": "Klasiko",
@ -1002,9 +1003,9 @@
"endGameText": "Itigil ang Laro", "endGameText": "Itigil ang Laro",
"exitGameText": "Umalis sa Laro", "exitGameText": "Umalis sa Laro",
"exitToMenuText": "Balik sa menu?", "exitToMenuText": "Balik sa menu?",
"howToPlayText": "Paano maglaro", "howToPlayText": "Paano Maglaro",
"justPlayerText": "(Si ${NAME} lang)", "justPlayerText": "(Si ${NAME} lang)",
"leaveGameText": "Ialis sa Laro", "leaveGameText": "Umalis sa Laro",
"leavePartyConfirmText": "Talagang aalis sa party na ito?", "leavePartyConfirmText": "Talagang aalis sa party na ito?",
"leavePartyText": "Umalis sa Party", "leavePartyText": "Umalis sa Party",
"quitText": "Umalis", "quitText": "Umalis",
@ -1368,6 +1369,7 @@
"tournamentStandingsText": "Mga Paninindigan sa Paligsahan", "tournamentStandingsText": "Mga Paninindigan sa Paligsahan",
"tournamentText": "Paligsahan", "tournamentText": "Paligsahan",
"tournamentTimeExpiredText": "Na-expire Na Ang Oras Ng 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", "tournamentsText": "Mga Paligsahan",
"translations": { "translations": {
"characterNames": { "characterNames": {
@ -1599,12 +1601,12 @@
"Invalid purchase.": "Di-wastong bilihin", "Invalid purchase.": "Di-wastong bilihin",
"Invalid tournament entry; score will be ignored.": "Di-wastong entry sa paligsahan; hindi papansinin ang mga iskor.", "Invalid tournament entry; score will be ignored.": "Di-wastong entry sa paligsahan; hindi papansinin ang mga iskor.",
"Item unlocked!": "Na-unlock ang aytem!", "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?", "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 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.", "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.", "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.", "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 \"${NAME}\" upgraded successfully.": "Matagumpay na na-upgrade ang profile na \"${NAME}\".",
"Profile could not be upgraded.": "Hindi ma-upgrade ang profile.", "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.", "workspaceSyncReuseText": "Hindi ma-sync ang ${WORKSPACE}. Muling paggamit ng nakaraang naka-sync na bersyon.",
"worldScoresUnavailableText": "Ang scores sa buong mundo ay hindi pa handa", "worldScoresUnavailableText": "Ang scores sa buong mundo ay hindi pa handa",
"worldsBestScoresText": "Pinakamahusay na Iskor ng Mundo", "worldsBestScoresText": "Pinakamahusay na Iskor ng Mundo",
"worldsBestTimesText": "Pinakamahusay na Oras sa Mundo", "worldsBestTimesText": "Oras ng Pinakamahusay sa Mundo",
"xbox360ControllersWindow": { "xbox360ControllersWindow": {
"getDriverText": "Kunin ang Driver", "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.", "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 :", "achievementsRemainingText": "Succès restant à remporter :",
"achievementsText": "Succès", "achievementsText": "Succès",
"achievementsUnavailableForOldSeasonsText": "Désolé, les spécifications des succès ne sont pas disponibles pour les saisons passées.", "achievementsUnavailableForOldSeasonsText": "Désolé, les spécifications des succès ne sont pas disponibles pour les saisons passées.",
"activatedText": "${THING} activé.",
"addGameWindow": { "addGameWindow": {
"getMoreGamesText": "Obtenir plus de jeux...", "getMoreGamesText": "Obtenir plus de jeux...",
"titleText": "Ajouter un Jeu" "titleText": "Ajouter un Jeu"
}, },
"allowText": "Autoriser", "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}.", "apiVersionErrorText": "Impossible de charger le jeu ${NAME}; sa version api est ${VERSION_USED}; nous demandons la version ${VERSION_REQUIRED}.",
"audioSettingsWindow": { "audioSettingsWindow": {
"headRelativeVRAudioInfoText": "(\"Auto\" s'active seulement quand un casque est branché)", "headRelativeVRAudioInfoText": "(\"Auto\" s'active seulement quand un casque est branché)",
@ -363,7 +364,7 @@
"boostText": "Accroître", "boostText": "Accroître",
"bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} est configuré dans sa propre application.", "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} est configuré dans sa propre application.",
"buttonText": "bouton", "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", "cancelText": "Annuler",
"cantConfigureDeviceText": "Désolé, ${DEVICE} ne peut pas être configuré.", "cantConfigureDeviceText": "Désolé, ${DEVICE} ne peut pas être configuré.",
"challengeEndedText": "Ce défi est terminé.", "challengeEndedText": "Ce défi est terminé.",
@ -381,7 +382,7 @@
"configureMobileText": "Appareils Mobiles comme Manettes", "configureMobileText": "Appareils Mobiles comme Manettes",
"configureTouchText": "Configurer l'Ecran Tactile", "configureTouchText": "Configurer l'Ecran Tactile",
"ps3Text": "Manettes de PS3", "ps3Text": "Manettes de PS3",
"titleText": "Mannettes", "titleText": "Manettes",
"wiimotesText": "Wiimotes", "wiimotesText": "Wiimotes",
"xbox360Text": "Manettes Xbox 360" "xbox360Text": "Manettes Xbox 360"
}, },
@ -412,7 +413,7 @@
"ignoredButton4Text": "Bouton Ignoré 4", "ignoredButton4Text": "Bouton Ignoré 4",
"ignoredButtonDescriptionText": "(utilisez ceci pour empêcher les boutons 'home' ou 'sync' dinterférer avec l'interface)", "ignoredButtonDescriptionText": "(utilisez ceci pour empêcher les boutons 'home' ou 'sync' dinterférer avec l'interface)",
"ignoredButtonText": "Bouton Ignoré", "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...", "pressAnyButtonOrDpadText": "Appuyez sur n'importe quel bouton ou croix directionnelle...",
"pressAnyButtonText": "Appuyez sur n'importe quel bouton...", "pressAnyButtonText": "Appuyez sur n'importe quel bouton...",
"pressLeftRightText": "Appuyez à gauche ou à droite...", "pressLeftRightText": "Appuyez à gauche ou à droite...",
@ -475,7 +476,7 @@
"activenessInfoText": "Ce bonus multiplicateur augmente lorsque vous jouez\net baisse quand vous ne jouez pas.", "activenessInfoText": "Ce bonus multiplicateur augmente lorsque vous jouez\net baisse quand vous ne jouez pas.",
"activityText": "Activité", "activityText": "Activité",
"campaignText": "Campagne", "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", "challengesText": "Défis",
"currentBestText": "Meilleur Score Actuel", "currentBestText": "Meilleur Score Actuel",
"customText": "Personnaliser", "customText": "Personnaliser",
@ -504,11 +505,12 @@
"titleText": "Co-op", "titleText": "Co-op",
"toRankedText": "Avant d'Être Classé", "toRankedText": "Avant d'Être Classé",
"totalText": "total", "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.", "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.", "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:" "yourPowerRankingText": "Votre Classement Mondial:"
}, },
"copyConfirmText": "Copié dans le presse papier.",
"copyOfText": "Copie de ${NAME}", "copyOfText": "Copie de ${NAME}",
"copyText": "Copier", "copyText": "Copier",
"copyrightText": "© 2013 Eric Froemling", "copyrightText": "© 2013 Eric Froemling",
@ -518,7 +520,7 @@
"creditsWindow": { "creditsWindow": {
"additionalAudioArtIdeasText": "Son Additionnel, Design Initial, et Idées par ${NAME}", "additionalAudioArtIdeasText": "Son Additionnel, Design Initial, et Idées par ${NAME}",
"additionalMusicFromText": "Musique additionnelle 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}", "codingGraphicsAudioText": "Codage, Graphiques, et Audio par ${NAME}",
"languageTranslationsText": "Traductions:", "languageTranslationsText": "Traductions:",
"legalText": "Légal:", "legalText": "Légal:",
@ -542,14 +544,14 @@
"runCPUBenchmarkText": "Lancer le test CPU (processeur)", "runCPUBenchmarkText": "Lancer le test CPU (processeur)",
"runGPUBenchmarkText": "Lancer le test GPU (carte graphique)", "runGPUBenchmarkText": "Lancer le test GPU (carte graphique)",
"runMediaReloadBenchmarkText": "Lancer le test de Media-Reload", "runMediaReloadBenchmarkText": "Lancer le test de Media-Reload",
"runStressTestText": "Test de robustèsse", "runStressTestText": "Test de robustesse",
"stressTestPlayerCountText": "Nombre de Joueurs", "stressTestPlayerCountText": "Nombre de Joueurs",
"stressTestPlaylistDescriptionText": "Playlist des tests de robustèsse", "stressTestPlaylistDescriptionText": "Playlist des tests de robustesse",
"stressTestPlaylistNameText": "Nom de la Playlist", "stressTestPlaylistNameText": "Nom de la Playlist",
"stressTestPlaylistTypeText": "Genre de la Playlist", "stressTestPlaylistTypeText": "Genre de la Playlist",
"stressTestRoundDurationText": "Durée du Niveau", "stressTestRoundDurationText": "Durée du Niveau",
"stressTestTitleText": "Test de robustèsse", "stressTestTitleText": "Test de robustesse",
"titleText": "Tests graphiques/processeur & de robustèsse", "titleText": "Tests graphiques/processeur & de robustesse",
"totalReloadTimeText": "Temps de redémarrage total: ${TIME} (référez-vous au registre plus de détails)", "totalReloadTimeText": "Temps de redémarrage total: ${TIME} (référez-vous au registre plus de détails)",
"unlockCoopText": "Débloquer les niveaux co-op" "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!?!?!", "difficultyHardUnlockOnlyText": "Ce niveau ne peut être débloqué qu'en mode difficile.\nPensez-vous être à la hauteur!?!?!",
"directBrowserToURLText": "Entrez cette URL dans un navigateur:", "directBrowserToURLText": "Entrez cette URL dans un navigateur:",
"disableRemoteAppConnectionsText": "Désactiver les connexions d'applications-manettes", "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", "disableXInputText": "Désactiver XInput",
"doneText": "Terminé", "doneText": "Terminé",
"drawText": "Égalité", "drawText": "Égalité",
@ -588,7 +590,7 @@
"titleText": "Éditeur de Playlist" "titleText": "Éditeur de Playlist"
}, },
"editProfileWindow": { "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)", "accountProfileText": "(profil du compte)",
"availableText": "Le nom \"${NAME}\" est disponible.", "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", "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", "titleEditText": "Éditer ce Profil",
"titleNewText": "Nouveau Profil", "titleNewText": "Nouveau Profil",
"unavailableText": "\"${NAME}\" n'est pas disponible; essayez un autre nom.", "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" "upgradeToGlobalProfileText": "Passer à un Profil Mondial"
}, },
"editProfilesAnyTimeText": "(vous pouvez éditer les profils à tout moment dans 'paramètres')", "editProfilesAnyTimeText": "(vous pouvez éditer les profils à tout moment dans 'paramètres')",
@ -625,7 +627,7 @@
"deleteConfirmText": "Effacer la Bande Sonore:\n\n'${NAME}'?", "deleteConfirmText": "Effacer la Bande Sonore:\n\n'${NAME}'?",
"deleteText": "Effacer la \nBande Sonore", "deleteText": "Effacer la \nBande Sonore",
"duplicateText": "Dupliquer la\nBande Sonore", "duplicateText": "Dupliquer la\nBande Sonore",
"editSoundtrackText": "Éditeur de Bandes Sonore", "editSoundtrackText": "Éditeur de Bandes Sonores",
"editText": "Modifier la\nBande Sonore", "editText": "Modifier la\nBande Sonore",
"fetchingITunesText": "chercher des playlists Music App", "fetchingITunesText": "chercher des playlists Music App",
"musicVolumeZeroWarning": "Attention: le volume de la musique est à 0", "musicVolumeZeroWarning": "Attention: le volume de la musique est à 0",
@ -649,7 +651,9 @@
"epicDescriptionFilterText": "${DESCRIPTION} Dans un \"slow-motion\" épique.", "epicDescriptionFilterText": "${DESCRIPTION} Dans un \"slow-motion\" épique.",
"epicNameFilterText": "${NAME} Épique", "epicNameFilterText": "${NAME} Épique",
"errorAccessDeniedText": "accès refusé", "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", "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", "errorText": "Erreur",
"errorUnknownText": "erreur inconnue", "errorUnknownText": "erreur inconnue",
"exitGameText": "Quitter ${APP_NAME}?", "exitGameText": "Quitter ${APP_NAME}?",
@ -716,7 +720,7 @@
"checkingText": "vérification...", "checkingText": "vérification...",
"copyCodeConfirmText": "Le code a bien été copié dans le presse-papier.", "copyCodeConfirmText": "Le code a bien été copié dans le presse-papier.",
"copyCodeText": "Copier le code", "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?", "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)", "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...", "earnTicketsForRecommendingText": "Partagez le jeu pour \ndes tickets gratuits...",
@ -736,7 +740,7 @@
"getFriendInviteCodeText": "Obtenir un Code pour Inviter mes Amis", "getFriendInviteCodeText": "Obtenir un Code pour Inviter mes Amis",
"googlePlayDescriptionText": "Invitez des joueurs Google Play à votre partie:", "googlePlayDescriptionText": "Invitez des joueurs Google Play à votre partie:",
"googlePlayInviteText": "Inviter", "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", "googlePlaySeeInvitesText": "Voir les Invitations",
"googlePlayText": "Google Play", "googlePlayText": "Google Play",
"googlePlayVersionOnlyText": "(Version Android / Google Play)", "googlePlayVersionOnlyText": "(Version Android / Google Play)",
@ -749,8 +753,8 @@
"joinPublicPartyDescriptionText": "Rejoindre une Partie Publique", "joinPublicPartyDescriptionText": "Rejoindre une Partie Publique",
"localNetworkDescriptionText": "Rejoindre une partie sur votre réseau (LAN, Bluetooth, Wi-Fi, etc.)", "localNetworkDescriptionText": "Rejoindre une partie sur votre réseau (LAN, Bluetooth, Wi-Fi, etc.)",
"localNetworkText": "Réseau Local", "localNetworkText": "Réseau Local",
"makePartyPrivateText": "Rend Ma Partie Privée", "makePartyPrivateText": "Rendre Ma Partie Privée",
"makePartyPublicText": "Rend Ma Partie Publique", "makePartyPublicText": "Rendre Ma Partie Publique",
"manualAddressText": "Adresse", "manualAddressText": "Adresse",
"manualConnectText": "Connexion", "manualConnectText": "Connexion",
"manualDescriptionText": "Joindre une partie par adresse:", "manualDescriptionText": "Joindre une partie par adresse:",
@ -827,7 +831,7 @@
"ticketPack4Text": "Pack de tickets géant", "ticketPack4Text": "Pack de tickets géant",
"ticketPack5Text": "Énorme pack de tickets", "ticketPack5Text": "Énorme pack de tickets",
"ticketPack6Text": "Ultime 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", "ticketsText": "${COUNT} Tickets",
"titleText": "Plus de 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.", "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" "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", "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", "googlePlayText": "Google Play",
"graphicsSettingsWindow": { "graphicsSettingsWindow": {
"alwaysText": "Toujours", "alwaysText": "Toujours",
@ -1075,7 +1080,7 @@
"modeClassicText": "Mode classique", "modeClassicText": "Mode classique",
"modeDemoText": "Mode Demo", "modeDemoText": "Mode Demo",
"mostValuablePlayerText": "Meilleur joueur", "mostValuablePlayerText": "Meilleur joueur",
"mostViolatedPlayerText": "Joueur le plus violé", "mostViolatedPlayerText": "Joueur le plus violenté",
"mostViolentPlayerText": "Joueur le plus violent", "mostViolentPlayerText": "Joueur le plus violent",
"moveText": "Bouger", "moveText": "Bouger",
"multiKillText": "${COUNT}-MEURTRES!!!", "multiKillText": "${COUNT}-MEURTRES!!!",
@ -1173,7 +1178,10 @@
"playlistsText": "Playlists", "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", "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...", "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", "pluginsText": "Plugins",
"practiceText": "Entraînement", "practiceText": "Entraînement",
"pressAnyButtonPlayAgainText": "Appuyez n'importe quel bouton pour rejouer...", "pressAnyButtonPlayAgainText": "Appuyez n'importe quel bouton pour rejouer...",
@ -1436,6 +1444,7 @@
"tournamentStandingsText": "Classements du Tournoi", "tournamentStandingsText": "Classements du Tournoi",
"tournamentText": "Tournoi", "tournamentText": "Tournoi",
"tournamentTimeExpiredText": "Le temps de ce tournoi a expiré", "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", "tournamentsText": "Tournois",
"translations": { "translations": {
"characterNames": { "characterNames": {
@ -1949,6 +1958,8 @@
"winsPlayerText": "${NAME} a Gagné!", "winsPlayerText": "${NAME} a Gagné!",
"winsTeamText": "${NAME} a Gagné!", "winsTeamText": "${NAME} a Gagné!",
"winsText": "${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.", "worldScoresUnavailableText": "Les scores mondial sont indisponibles.",
"worldsBestScoresText": "Meilleurs scores mondiaux", "worldsBestScoresText": "Meilleurs scores mondiaux",
"worldsBestTimesText": "Meilleurs temps mondiaux", "worldsBestTimesText": "Meilleurs temps mondiaux",

View file

@ -331,6 +331,7 @@
"achievementsRemainingText": "Fehlende Erfolge:", "achievementsRemainingText": "Fehlende Erfolge:",
"achievementsText": "Erfolge", "achievementsText": "Erfolge",
"achievementsUnavailableForOldSeasonsText": "Leider Leistung Besonderheiten nicht für alte Jahreszeiten zur Verfügung.", "achievementsUnavailableForOldSeasonsText": "Leider Leistung Besonderheiten nicht für alte Jahreszeiten zur Verfügung.",
"activatedText": "${THING} aktiviert.",
"addGameWindow": { "addGameWindow": {
"getMoreGamesText": "Hol dir mehr Spiele...", "getMoreGamesText": "Hol dir mehr Spiele...",
"titleText": "Spiel hinzufügen", "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 .", "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:" "yourPowerRankingText": "Dein Power Rang:"
}, },
"copyConfirmText": "In die Zwischenablage kopiert.",
"copyOfText": "${NAME} Kopieren", "copyOfText": "${NAME} Kopieren",
"copyText": "Kopieren", "copyText": "Kopieren",
"copyrightText": "© 2013 Eric Froemling", "copyrightText": "© 2013 Eric Froemling",
@ -658,7 +660,9 @@
"epicDescriptionFilterText": "${DESCRIPTION} In epischer Zeitlupe.", "epicDescriptionFilterText": "${DESCRIPTION} In epischer Zeitlupe.",
"epicNameFilterText": "Episch ${NAME}", "epicNameFilterText": "Episch ${NAME}",
"errorAccessDeniedText": "Zugriff verweigert", "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", "errorOutOfDiskSpaceText": "Nicht genug Speicherplatz",
"errorSecureConnectionFailText": "Nicht möglich, eine sichere Cloud-Verbindung herzustellen; Netzwerk funktionalität kann versagen.",
"errorText": "Fehler", "errorText": "Fehler",
"errorUnknownText": "unbekannter Fehler", "errorUnknownText": "unbekannter Fehler",
"exitGameText": "${APP_NAME} verlassen?", "exitGameText": "${APP_NAME} verlassen?",
@ -836,7 +840,7 @@
"ticketPack4Text": "Riesiges Ticketpack", "ticketPack4Text": "Riesiges Ticketpack",
"ticketPack5Text": "Kolossales Ticketpack", "ticketPack5Text": "Kolossales Ticketpack",
"ticketPack6Text": "Ultimatives Ticketpack", "ticketPack6Text": "Ultimatives Ticketpack",
"ticketsFromASponsorText": "Bekomme ${COUNT} Tickets\ndurch Werbung", "ticketsFromASponsorText": "Sehen Sie sich eine Anzeige an\nfür ${COUNT} Tickets",
"ticketsText": "${COUNT} Tickets", "ticketsText": "${COUNT} Tickets",
"titleText": "Hol dir 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.", "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" "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", "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", "googlePlayText": "Google Play",
"graphicsSettingsWindow": { "graphicsSettingsWindow": {
"alwaysText": "Immer", "alwaysText": "Immer",
@ -1191,7 +1196,10 @@
"playlistsText": "Playlists", "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", "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...", "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.", "pluginsDetectedText": "Neue Plugins erkannt. Neustarten, um sie zu aktivieren oder in den Einstellungen konfigurieren.",
"pluginsRemovedText": "${NUM} plugin(s) wurden nicht mehr gefunden.",
"pluginsText": "Plugins", "pluginsText": "Plugins",
"practiceText": "Übung", "practiceText": "Übung",
"pressAnyButtonPlayAgainText": "Drücke einen Knopf um nochmal zu spielen...", "pressAnyButtonPlayAgainText": "Drücke einen Knopf um nochmal zu spielen...",
@ -1459,6 +1467,7 @@
"tournamentStandingsText": "Tournier Tabelle", "tournamentStandingsText": "Tournier Tabelle",
"tournamentText": "Turnier", "tournamentText": "Turnier",
"tournamentTimeExpiredText": "Turnierzeit abgelaufen", "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", "tournamentsText": "Turniere",
"translations": { "translations": {
"characterNames": { "characterNames": {
@ -1979,6 +1988,8 @@
"winsPlayerText": "${NAME} Gewinnt!", "winsPlayerText": "${NAME} Gewinnt!",
"winsTeamText": "${NAME} Gewinnt!", "winsTeamText": "${NAME} Gewinnt!",
"winsText": "${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", "worldScoresUnavailableText": "Weltrangliste ist nicht verfügbar",
"worldsBestScoresText": "Beste Punktzahl weltweit", "worldsBestScoresText": "Beste Punktzahl weltweit",
"worldsBestTimesText": "Beste Zeit 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.", "welcome2Text": "Yz cm alfj fcojwfowiejfo wiejo wfoinoaicoiwefoiwef.\nTickef woioiweofiw efoiauoicoiwefjoaieofaefa\nminf-fizoj , and itner ouacohao,a nd fmofz.",
"yourPowerRankingText": "Yrrlz Powe Rnkkffz:" "yourPowerRankingText": "Yrrlz Powe Rnkkffz:"
}, },
"copyConfirmText": "Cpoew cow owes oC.",
"copyOfText": "Copzyz du ${NAME}", "copyOfText": "Copzyz du ${NAME}",
"copyText": "Czópy", "copyText": "Czópy",
"copyrightText": "© 2013 Eric Froemling", "copyrightText": "© 2013 Eric Froemling",
@ -661,7 +662,7 @@
"epicDescriptionFilterText": "${DESCRIPTION} Ín ípic slúw mztíon.", "epicDescriptionFilterText": "${DESCRIPTION} Ín ípic slúw mztíon.",
"epicNameFilterText": "${NAME} Epícz", "epicNameFilterText": "${NAME} Epícz",
"errorAccessDeniedText": "acczlr dnfflz", "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", "errorOutOfDiskSpaceText": "orz of dkzk spzlfz",
"errorSecureConnectionFailText": "Uweorcjwoef ojcowe werryyeoi nowe; jcnwoeroidfowdffdj.dfsdf.", "errorSecureConnectionFailText": "Uweorcjwoef ojcowe werryyeoi nowe; jcnwoeroidfowdffdj.dfsdf.",
"errorText": "Errórz", "errorText": "Errórz",
@ -730,6 +731,7 @@
"checkingText": "chzckinggz..", "checkingText": "chzckinggz..",
"copyCodeConfirmText": "Cdf cpodf to clfjoifjz.", "copyCodeConfirmText": "Cdf cpodf to clfjoifjz.",
"copyCodeText": "Cpoef Cwfdf", "copyCodeText": "Cpoef Cwfdf",
"copyConfirmText": "COpic for cowejwdf.",
"dedicatedServerInfoText": "For code wocj woiejfowiejf, loci joweijf owiejfw. Se eocwj efowiejo wcoweijf woeifowoco er.", "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?", "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)", "earnTicketsForRecommendingAmountText": "Fofofj oicow ${COUNT} ocwjoe f cow ef woefje\n(aocweo fwjoefi jo${YOU_COUNT} cowiejfowi oie)",
@ -1475,6 +1477,7 @@
"tournamentStandingsText": "Tzewfjwoij Stndfalfjz", "tournamentStandingsText": "Tzewfjwoij Stndfalfjz",
"tournamentText": "Tanfowijfowef", "tournamentText": "Tanfowijfowef",
"tournamentTimeExpiredText": "Tmcoef Tm Epzoiejfefz", "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", "tournamentsText": "Trzzmfnmflfzzs",
"translations": { "translations": {
"characterNames": { "characterNames": {

View file

@ -325,6 +325,7 @@
"achievementsRemainingText": "Υπολειπόμενα Επιτεύγματα:", "achievementsRemainingText": "Υπολειπόμενα Επιτεύγματα:",
"achievementsText": "Επιτεύγματα", "achievementsText": "Επιτεύγματα",
"achievementsUnavailableForOldSeasonsText": "Συγνώμη, ακριβείς λεπτομέρειες σχετικές με τα επιτεύγματα είναι μη διαθέσιμες για παλαιότερες σεζόν.", "achievementsUnavailableForOldSeasonsText": "Συγνώμη, ακριβείς λεπτομέρειες σχετικές με τα επιτεύγματα είναι μη διαθέσιμες για παλαιότερες σεζόν.",
"activatedText": "Το ${THING} ενεργοποιήθηκε.",
"addGameWindow": { "addGameWindow": {
"getMoreGamesText": "Περισσότερα Παιχνίδια...", "getMoreGamesText": "Περισσότερα Παιχνίδια...",
"titleText": "Προσθήκη Παιχνιδιού" "titleText": "Προσθήκη Παιχνιδιού"
@ -496,6 +497,7 @@
"welcome2Text": "Μπορείτε ακόμα να κερδίσετε εισιτήρια με πολλές παρόμοιες δραστηριότητες.\nΤα εισιτήρια μπορούν να χρησιμοποιηθούν για να ξεκλειδώσετε νέους\nχαρακτήρες, χάρτες και μικροπαιχνίδια, να συμμετάσχετε σε τουρνουά, κ.α.", "welcome2Text": "Μπορείτε ακόμα να κερδίσετε εισιτήρια με πολλές παρόμοιες δραστηριότητες.\nΤα εισιτήρια μπορούν να χρησιμοποιηθούν για να ξεκλειδώσετε νέους\nχαρακτήρες, χάρτες και μικροπαιχνίδια, να συμμετάσχετε σε τουρνουά, κ.α.",
"yourPowerRankingText": "Η Κατάταξη Δύναμής Σας:" "yourPowerRankingText": "Η Κατάταξη Δύναμής Σας:"
}, },
"copyConfirmText": "Αντιγράφτηκε στο πρόχειρο.",
"copyOfText": "${NAME} Αντίγραφο", "copyOfText": "${NAME} Αντίγραφο",
"copyText": "αντίγραφο", "copyText": "αντίγραφο",
"createEditPlayerText": "<Δημιουργία/Επεξεργασία Παίκτη>", "createEditPlayerText": "<Δημιουργία/Επεξεργασία Παίκτη>",
@ -623,7 +625,9 @@
"epicDescriptionFilterText": "${DESCRIPTION} Σε επικά αργή κίνηση.", "epicDescriptionFilterText": "${DESCRIPTION} Σε επικά αργή κίνηση.",
"epicNameFilterText": "${NAME} Επικό", "epicNameFilterText": "${NAME} Επικό",
"errorAccessDeniedText": "η πρόσβαση απορρίφθηκε", "errorAccessDeniedText": "η πρόσβαση απορρίφθηκε",
"errorDeviceTimeIncorrectText": "Η ώρα της συσκευής σας είναι λάθος ${HOURS} ώρες.\nΑυτο είναι πιθανό να πεοκλέσει προβλήματα.\nΠαρακαλούμε ελέξτε τις ρυθμίσεις ώρας σας.",
"errorOutOfDiskSpaceText": "έλλειψη αποθηκευτικού χώρου", "errorOutOfDiskSpaceText": "έλλειψη αποθηκευτικού χώρου",
"errorSecureConnectionFailText": "Αδύνατο να δημιουργηθεί ασφαλής σύνδεση cloud, η χρήση του διαδικτύου μπορεί να αποτύχει.",
"errorText": "Σφάλμα", "errorText": "Σφάλμα",
"errorUnknownText": "άγνωστο σφάλμα", "errorUnknownText": "άγνωστο σφάλμα",
"exitGameText": "Έξοδος από το ${APP_NAME};", "exitGameText": "Έξοδος από το ${APP_NAME};",
@ -786,7 +790,7 @@
"ticketPack4Text": "Πακέτο Εισιτηρίων Jumbo", "ticketPack4Text": "Πακέτο Εισιτηρίων Jumbo",
"ticketPack5Text": "Πακέτο Εισιτηρίων Μαμούθ", "ticketPack5Text": "Πακέτο Εισιτηρίων Μαμούθ",
"ticketPack6Text": "Υπέρτατο Πακέτο Εισιτηρίων", "ticketPack6Text": "Υπέρτατο Πακέτο Εισιτηρίων",
"ticketsFromASponsorText": "Αποκτήστε ${COUNT} εισιτήρια\nαπό χορηγία", "ticketsFromASponsorText": "Παρακολουθήστε μια διαφήμηση\nγια ${COUNT} εισιτήρια",
"ticketsText": "${COUNT} Εισιτήρια", "ticketsText": "${COUNT} Εισιτήρια",
"titleText": "Αποκτήστε Εισιτήρια", "titleText": "Αποκτήστε Εισιτήρια",
"unavailableLinkAccountText": "Συγνώμη, οι αγορές δεν είναι διαθέσιμες σε αυτή την πλατφόρμα.\nΩς λύση, μπορείτε να δεσμεύσετε αυτόν τον λογαριασμό με έναν\nλογαριασμό από άλλη πλατφόρμα και να αγοράσετε από εκεί.", "unavailableLinkAccountText": "Συγνώμη, οι αγορές δεν είναι διαθέσιμες σε αυτή την πλατφόρμα.\nΩς λύση, μπορείτε να δεσμεύσετε αυτόν τον λογαριασμό με έναν\nλογαριασμό από άλλη πλατφόρμα και να αγοράσετε από εκεί.",
@ -797,6 +801,7 @@
"youHaveText": "έχετε ${COUNT} εισιτήρια" "youHaveText": "έχετε ${COUNT} εισιτήρια"
}, },
"googleMultiplayerDiscontinuedText": "Συγνώμη, φαίνεται πως η υπηρεσία πολλών παικτών της Google δεν είναι πλέον διαθέσιμη.\nΠροσπαθώ να βρω αντικατάσταση όσο πιο γρήγορα γίνεται.\nΜέχρι τότε, παρακαλώ δοκιμάστε άλλο τρόπο σύνδεσης.\n-Eric", "googleMultiplayerDiscontinuedText": "Συγνώμη, φαίνεται πως η υπηρεσία πολλών παικτών της Google δεν είναι πλέον διαθέσιμη.\nΠροσπαθώ να βρω αντικατάσταση όσο πιο γρήγορα γίνεται.\nΜέχρι τότε, παρακαλώ δοκιμάστε άλλο τρόπο σύνδεσης.\n-Eric",
"googlePlayPurchasesNotAvailableText": "Οι αγορές Google Play δεν είναι διαθέσιμες.\nΜπορεί να χρειάζεται να ενημερώσετε την εφαρμογή σας.",
"googlePlayText": "Google Play", "googlePlayText": "Google Play",
"graphicsSettingsWindow": { "graphicsSettingsWindow": {
"alwaysText": "Πάντα", "alwaysText": "Πάντα",
@ -1111,7 +1116,10 @@
"playlistsText": "Λίστες Παιχνιδιών", "playlistsText": "Λίστες Παιχνιδιών",
"pleaseRateText": "Εάν απολαμβάνετε το ${APP_NAME}, παρακαλώ σκεφτείτε να αφιερώσετε μιά στιγμή\nγια να το βαθμολογήσετε ή να γράψετε μιά κριτική. Αυτό θα προσφέρει χρήσιμη\nανατροφοδότηση και θα βοηθήσει για την υποστήριξη της μέλλουσας ανάπτυξης.\n\nευχαριστώ!\n-eric", "pleaseRateText": "Εάν απολαμβάνετε το ${APP_NAME}, παρακαλώ σκεφτείτε να αφιερώσετε μιά στιγμή\nγια να το βαθμολογήσετε ή να γράψετε μιά κριτική. Αυτό θα προσφέρει χρήσιμη\nανατροφοδότηση και θα βοηθήσει για την υποστήριξη της μέλλουσας ανάπτυξης.\n\nευχαριστώ!\n-eric",
"pleaseWaitText": "Παρακαλώ περιμένετε...", "pleaseWaitText": "Παρακαλώ περιμένετε...",
"pluginsDetectedText": "Νέα πρόσθετο/α εντοπίστηκαν. Ενεργοποίηστε/Διαμορφώστε τα από τις ρυθμίσεις.", "pluginClassLoadErrorText": "Σφάλμα φορτώνοντας πρόσθετο '${PLUGIN}': ${ERROR}",
"pluginInitErrorText": "Σφάλμα επερξεγάζοντας πρόσθετο '${PLUGIN}': ${ERROR}",
"pluginsDetectedText": "Νέα πρόσθετο/α εντοπίστηκαν. Επανεκκινήστε την εφαρμογή για να τα ενεργοποιήσετε, ή διαμορφώστε τα στις ρυθμίσεις.",
"pluginsRemovedText": "${NUM} πρόσθετο/α δεν εντοπίζονται πια.",
"pluginsText": "Πρόσθετα", "pluginsText": "Πρόσθετα",
"practiceText": "Πρακτική", "practiceText": "Πρακτική",
"pressAnyButtonPlayAgainText": "Πατήστε οποιοδήποτε κουμπί για να ξαναπαίξετε...", "pressAnyButtonPlayAgainText": "Πατήστε οποιοδήποτε κουμπί για να ξαναπαίξετε...",
@ -1364,6 +1372,7 @@
"tournamentStandingsText": "Πίνακας Κατάταξης Τουρνουά", "tournamentStandingsText": "Πίνακας Κατάταξης Τουρνουά",
"tournamentText": "Τουρνουά", "tournamentText": "Τουρνουά",
"tournamentTimeExpiredText": "Ο Χρόνος του Τουρνουά Έληξε", "tournamentTimeExpiredText": "Ο Χρόνος του Τουρνουά Έληξε",
"tournamentsDisabledWorkspaceText": "Τα τουρνουά είναι απενεργοποιημένα όταν χόροι εργασίας είναι ενεργοί.\nΓια να το ενεργοποιήσετε, κλείστε τον χώρο εργασίας σας και επανεκκινήστε την εφαρμογή.",
"tournamentsText": "Τουρνουά", "tournamentsText": "Τουρνουά",
"translations": { "translations": {
"characterNames": { "characterNames": {
@ -1847,6 +1856,8 @@
"winsPlayerText": "Ο Παίκτης ${NAME} Νίκησε!", "winsPlayerText": "Ο Παίκτης ${NAME} Νίκησε!",
"winsTeamText": "Η Ομάδα ${NAME} Νίκησε!", "winsTeamText": "Η Ομάδα ${NAME} Νίκησε!",
"winsText": "${NAME} Νίκησε!", "winsText": "${NAME} Νίκησε!",
"workspaceSyncErrorText": "Σφάλμα συνγχρονήζοντας ${WORKSPACE}. Δείτε την καταγραφή για λεπτομέρειες.",
"workspaceSyncReuseText": "Ο χώρος εργασίας ${WORKSPACE} δεν μπορεί να συγχρονιστεί. Η προηγούμενη συγχρονισμένη έκδοση θα χρησιμοποιηθεί.",
"worldScoresUnavailableText": "Παγκόσμιες βαθμολογίες μη διαθέσιμες.", "worldScoresUnavailableText": "Παγκόσμιες βαθμολογίες μη διαθέσιμες.",
"worldsBestScoresText": "Καλύτερες Βαθμολογίες Παγκοσμίως", "worldsBestScoresText": "Καλύτερες Βαθμολογίες Παγκοσμίως",
"worldsBestTimesText": "Καλύτεροι Χρόνοι Παγκοσμίως", "worldsBestTimesText": "Καλύτεροι Χρόνοι Παγκοσμίως",

View file

@ -329,6 +329,7 @@
"achievementsRemainingText": "उप्लाब्धियाँ बाकी:", "achievementsRemainingText": "उप्लाब्धियाँ बाकी:",
"achievementsText": "उप्लाब्धियाँ", "achievementsText": "उप्लाब्धियाँ",
"achievementsUnavailableForOldSeasonsText": "माफ़ करें उपलब्धियों कि जानकारी पुराने सीजन से नहीं है", "achievementsUnavailableForOldSeasonsText": "माफ़ करें उपलब्धियों कि जानकारी पुराने सीजन से नहीं है",
"activatedText": "${THING} सक्रिय",
"addGameWindow": { "addGameWindow": {
"getMoreGamesText": "और गेम्स कि जानकारी पायें", "getMoreGamesText": "और गेम्स कि जानकारी पायें",
"titleText": "गेम जोड़ें" "titleText": "गेम जोड़ें"
@ -499,6 +500,7 @@
"welcome2Text": "आप टिकेट उन्ही गतिविधियाओं से भी कमा सकते हैं | \nटिकेट नए रूप, नक़्शे व छोटे गेम खोलने तथा \nप्रतियोगिता में भाग लेने आदि में काम आ सकते हैं |", "welcome2Text": "आप टिकेट उन्ही गतिविधियाओं से भी कमा सकते हैं | \nटिकेट नए रूप, नक़्शे व छोटे गेम खोलने तथा \nप्रतियोगिता में भाग लेने आदि में काम आ सकते हैं |",
"yourPowerRankingText": "आपका सत्ता पद :" "yourPowerRankingText": "आपका सत्ता पद :"
}, },
"copyConfirmText": "क्लिपबोर्ड पर कॉपी हुआ।",
"copyOfText": "${NAME} दूसरा", "copyOfText": "${NAME} दूसरा",
"copyText": "नकल किजिए", "copyText": "नकल किजिए",
"createEditPlayerText": "<प्लेयर बनाएँ / संपादित करें>", "createEditPlayerText": "<प्लेयर बनाएँ / संपादित करें>",
@ -627,7 +629,9 @@
"epicDescriptionFilterText": "${DESCRIPTION} उत्कृष्ट धीमे गति में।", "epicDescriptionFilterText": "${DESCRIPTION} उत्कृष्ट धीमे गति में।",
"epicNameFilterText": "उत्कृष्ट ${NAME}", "epicNameFilterText": "उत्कृष्ट ${NAME}",
"errorAccessDeniedText": "अभिगम वर्जित", "errorAccessDeniedText": "अभिगम वर्जित",
"errorDeviceTimeIncorrectText": "आपके उपकरण का समय ${HOURS} घंटे गलत है।\nइससे समस्याएं होने की संभावना है।\nकृपया अपना समय और समय-क्षेत्र सेटिंग की जाँच करें",
"errorOutOfDiskSpaceText": "डिस्क पे जगह ख़तम", "errorOutOfDiskSpaceText": "डिस्क पे जगह ख़तम",
"errorSecureConnectionFailText": "सुरक्षित क्लाउड कनेक्शन स्थापित करने में असमर्थ; नेटवर्क कार्यक्षमता विफल हो सकती है",
"errorText": "त्रुटी", "errorText": "त्रुटी",
"errorUnknownText": "अज्ञात त्रुटी", "errorUnknownText": "अज्ञात त्रुटी",
"exitGameText": "${APP_NAME} से निकास करें ?", "exitGameText": "${APP_NAME} से निकास करें ?",
@ -792,7 +796,7 @@
"ticketPack4Text": "बहुत बड़ा टिकेट का संग्रह", "ticketPack4Text": "बहुत बड़ा टिकेट का संग्रह",
"ticketPack5Text": "महान टिकेट संग्रह", "ticketPack5Text": "महान टिकेट संग्रह",
"ticketPack6Text": "महाकाय टिकेट संग्रह", "ticketPack6Text": "महाकाय टिकेट संग्रह",
"ticketsFromASponsorText": "किसी प्रायोजक \nसे ${COUNT} पायें", "ticketsFromASponsorText": "एक विज्ञापन देख के \n${COUNT} टिकट प्राप्त करें",
"ticketsText": "${COUNT} टिकेट", "ticketsText": "${COUNT} टिकेट",
"titleText": "टिकेट पायें", "titleText": "टिकेट पायें",
"unavailableLinkAccountText": "माफ़ करें इस प्लेटफार्म पर खरीदारी नहीं कि जा सकती है |\nएक वैकल्पिक हल के रूप में आप इस खाते को किसी \nऔर प्लेटफार्म के खाते से जोड़ कर खरीदारी कर सकते हैं |", "unavailableLinkAccountText": "माफ़ करें इस प्लेटफार्म पर खरीदारी नहीं कि जा सकती है |\nएक वैकल्पिक हल के रूप में आप इस खाते को किसी \nऔर प्लेटफार्म के खाते से जोड़ कर खरीदारी कर सकते हैं |",
@ -803,6 +807,7 @@
"youHaveText": "आपके पास ${COUNT} टिकेट हैं" "youHaveText": "आपके पास ${COUNT} टिकेट हैं"
}, },
"googleMultiplayerDiscontinuedText": "क्षमा करें, गूगल की एक साथ खेलने की सेवा अब उपलब्ध नहीं है। \nमैं जितनी जल्दी हो सके एक प्रतिस्थापन पर काम कर रहा हूं।\nतब तक, कृपया दूसरी जुडने की विधि आज़माएँ। \n-Eric", "googleMultiplayerDiscontinuedText": "क्षमा करें, गूगल की एक साथ खेलने की सेवा अब उपलब्ध नहीं है। \nमैं जितनी जल्दी हो सके एक प्रतिस्थापन पर काम कर रहा हूं।\nतब तक, कृपया दूसरी जुडने की विधि आज़माएँ। \n-Eric",
"googlePlayPurchasesNotAvailableText": "गूगल प्ले से ख़रीदारी उपलब्ध नहीं हैं।\nआपको अपना स्टोर ऐप अपडेट करना पड़ सकता है।",
"googlePlayText": "गूगल प्ले", "googlePlayText": "गूगल प्ले",
"graphicsSettingsWindow": { "graphicsSettingsWindow": {
"alwaysText": "हमेशा", "alwaysText": "हमेशा",
@ -1116,7 +1121,10 @@
"playlistsText": "प्लेलिस्ट", "playlistsText": "प्लेलिस्ट",
"pleaseRateText": "अगर आपको ${APP_NAME} में मज़ा आ रहा है, \nतो एक क्षण ले कर इसका मूल्यांकन करें | \nयह इस गेम के आगे के विकास का एक बहुत अहम् अंश है | \n\nधन्यवाद !\n-एरिक", "pleaseRateText": "अगर आपको ${APP_NAME} में मज़ा आ रहा है, \nतो एक क्षण ले कर इसका मूल्यांकन करें | \nयह इस गेम के आगे के विकास का एक बहुत अहम् अंश है | \n\nधन्यवाद !\n-एरिक",
"pleaseWaitText": "कृपया प्रतीक्षा करें...", "pleaseWaitText": "कृपया प्रतीक्षा करें...",
"pluginsDetectedText": "नए प्लगइन्स पता चले। उन्हें सेटिंग्स में चालू / कॉन्फ़िगर करें।", "pluginClassLoadErrorText": "प्लगइन क्लास '${PLUGIN}' लोड करने में त्रुटि: ${ERROR}",
"pluginInitErrorText": "प्लगइन '${PLUGIN}' शुरुआत करने में त्रुटि: ${ERROR}",
"pluginsDetectedText": "नए प्लगइन्स पता चले। उन्हें सक्रिय करने के लिए पुनरारंभ करें, या उन्हें सेटिंग्स में कॉन्फ़िगर करें।",
"pluginsRemovedText": "${NUM} प्लगइन्स अब नहीं मिले।",
"pluginsText": "प्लगइन्स", "pluginsText": "प्लगइन्स",
"practiceText": "अभ्यास", "practiceText": "अभ्यास",
"pressAnyButtonPlayAgainText": "दोबारा खेलने के लिए कोई भी बटन दबाएँ...", "pressAnyButtonPlayAgainText": "दोबारा खेलने के लिए कोई भी बटन दबाएँ...",
@ -1368,6 +1376,7 @@
"tournamentStandingsText": "प्रतियोगिता स्टैंडिंग्स", "tournamentStandingsText": "प्रतियोगिता स्टैंडिंग्स",
"tournamentText": "प्रतियोगिता", "tournamentText": "प्रतियोगिता",
"tournamentTimeExpiredText": "प्रतियोगिता समय समाप्त", "tournamentTimeExpiredText": "प्रतियोगिता समय समाप्त",
"tournamentsDisabledWorkspaceText": "कार्यस्थान सक्रिय होने पर टूर्नामेंट अक्षम हो जाते हैं।\nटूर्नामेंट को पुन: सक्षम करने के लिए, अपने कार्यक्षेत्र को अक्षम करें और पुनः आरंभ करें।",
"tournamentsText": "प्रतियोगिता", "tournamentsText": "प्रतियोगिता",
"translations": { "translations": {
"characterNames": { "characterNames": {
@ -1851,6 +1860,8 @@
"winsPlayerText": "${NAME} विजयी!", "winsPlayerText": "${NAME} विजयी!",
"winsTeamText": "${NAME} विजयी!", "winsTeamText": "${NAME} विजयी!",
"winsText": "${NAME} विजयी!", "winsText": "${NAME} विजयी!",
"workspaceSyncErrorText": "${WORKSPACE} सिंक में त्रुटि। विवरण के लिए लॉग देखें।",
"workspaceSyncReuseText": "${WORKSPACE} सिंक करने में असमर्थ। पिछले समन्वयित संस्करण का पुन: उपयोग होगा।",
"worldScoresUnavailableText": "वैश्विक अंक उपलब्ध नहीं", "worldScoresUnavailableText": "वैश्विक अंक उपलब्ध नहीं",
"worldsBestScoresText": "जागतिक सर्वोत्तम स्कोर्स", "worldsBestScoresText": "जागतिक सर्वोत्तम स्कोर्स",
"worldsBestTimesText": "विश्व का सबसे अधिक समय", "worldsBestTimesText": "विश्व का सबसे अधिक समय",

View file

@ -1245,7 +1245,7 @@
"disableCameraShakeText": "Kamera rázást Kikapcsolni", "disableCameraShakeText": "Kamera rázást Kikapcsolni",
"disableThisNotice": "(kikapcsolhatod ezt a beállítások menüben)", "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)", "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", "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.", "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!", "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:", "achievementsRemainingText": "Achievement Tersisa:",
"achievementsText": "Achievement", "achievementsText": "Achievement",
"achievementsUnavailableForOldSeasonsText": "Maaf, spesifik achievement tidak tersedia untuk musim lama.", "achievementsUnavailableForOldSeasonsText": "Maaf, spesifik achievement tidak tersedia untuk musim lama.",
"activatedText": "${THING} telah aktif",
"addGameWindow": { "addGameWindow": {
"getMoreGamesText": "Game Lain...", "getMoreGamesText": "Game Lain...",
"titleText": "Tambah Game" "titleText": "Tambah Game"
@ -433,13 +434,13 @@
"actionsText": "Aksi", "actionsText": "Aksi",
"buttonsText": "tombol", "buttonsText": "tombol",
"dragControlsText": "< geser kontrol untuk memposisikannya >", "dragControlsText": "< geser kontrol untuk memposisikannya >",
"joystickText": "Joystick", "joystickText": "joystick",
"movementControlScaleText": "Skala kontrol penggerak", "movementControlScaleText": "Skala kontrol penggerak",
"movementText": "Pergerakan", "movementText": "Pergerakan",
"resetText": "Kembalikan ke awal", "resetText": "Kembalikan ke awal",
"swipeControlsHiddenText": "Sembunyikan ikon geser", "swipeControlsHiddenText": "Sembunyikan ikon geser",
"swipeInfoText": "Model kontrol 'geser' membutuhkan penggunaan sedikit \nnamun membuat mudah untuk bermain tanpa melihat pengontrol", "swipeInfoText": "Model kontrol 'geser' membutuhkan penggunaan sedikit \nnamun membuat mudah untuk bermain tanpa melihat pengontrol",
"swipeText": "Geser", "swipeText": "geser",
"titleText": "Atur layar sentuh" "titleText": "Atur layar sentuh"
}, },
"configureItNowText": "Atur sekarang?", "configureItNowText": "Atur sekarang?",
@ -453,7 +454,7 @@
"forIOSText": "Untuk iOS:", "forIOSText": "Untuk iOS:",
"getItForText": "Dapatkan ${REMOTE_APP_NAME} untuk iOS di Apple App Store\natau untuk Android di Google Play Store atau Amazon Appstore", "getItForText": "Dapatkan ${REMOTE_APP_NAME} untuk iOS di Apple App Store\natau untuk Android di Google Play Store atau Amazon Appstore",
"googlePlayText": "Google Play", "googlePlayText": "Google Play",
"titleText": "Gunakan Perangkat untuk kntroler:" "titleText": "Gunakan Perangkat untuk kontroler:"
}, },
"continuePurchaseText": "Lanjutkan untuk ${PRICE}?", "continuePurchaseText": "Lanjutkan untuk ${PRICE}?",
"continueText": "Lanjutkan", "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", "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:" "yourPowerRankingText": "Peringkat Kekuatan Kamu:"
}, },
"copyConfirmText": "Tersalin ke papan klip.",
"copyOfText": "Salinan ${NAME}", "copyOfText": "Salinan ${NAME}",
"copyText": "Salin", "copyText": "Salin",
"createEditPlayerText": "<Buat/Edit pemain>", "createEditPlayerText": "<Buat/Edit pemain>",
@ -529,20 +531,20 @@
"runMediaReloadBenchmarkText": "Menjalankan Media-Reload Benchmark", "runMediaReloadBenchmarkText": "Menjalankan Media-Reload Benchmark",
"runStressTestText": "Menjalankan test stress", "runStressTestText": "Menjalankan test stress",
"stressTestPlayerCountText": "Jumlah Pemain", "stressTestPlayerCountText": "Jumlah Pemain",
"stressTestPlaylistDescriptionText": "Stress Test Playlist", "stressTestPlaylistDescriptionText": "Daftar Putar Stres Tes",
"stressTestPlaylistNameText": "Nama PLaylist", "stressTestPlaylistNameText": "Nama Daftar Putar",
"stressTestPlaylistTypeText": "Tipe Playlist", "stressTestPlaylistTypeText": "Tipe Daftar Putar",
"stressTestRoundDurationText": "Durasi Permainan", "stressTestRoundDurationText": "Durasi Permainan",
"stressTestTitleText": "Uji Stress", "stressTestTitleText": "Uji Stres",
"titleText": "Uji Benchmarks % Stress", "titleText": "Uji Tolak Ukur % Stres",
"totalReloadTimeText": "Total waktu memuat: ${TIME} (lihat log untuk selengkapnya)" "totalReloadTimeText": "Total waktu memuat: ${TIME} (lihat log untuk selengkapnya)"
}, },
"defaultGameListNameText": "Playlist ${PLAYMODE} Semula", "defaultGameListNameText": "Daftar Putar ${PLAYMODE} Semula",
"defaultNewGameListNameText": "Playlist ${PLAYMODE} Ku", "defaultNewGameListNameText": "Daftar Putar ${PLAYMODE} Ku",
"deleteText": "Hapus", "deleteText": "Hapus",
"demoText": "Demo", "demoText": "Demo",
"denyText": "Tolak", "denyText": "Tolak",
"desktopResText": "Desktop Res", "desktopResText": "Resolusi Desktop",
"difficultyEasyText": "Mudah", "difficultyEasyText": "Mudah",
"difficultyHardOnlyText": "Khusus Mode Sulit", "difficultyHardOnlyText": "Khusus Mode Sulit",
"difficultyHardText": "Sulit", "difficultyHardText": "Sulit",
@ -555,31 +557,31 @@
"drawText": "Seri", "drawText": "Seri",
"duplicateText": "Duplikat", "duplicateText": "Duplikat",
"editGameListWindow": { "editGameListWindow": {
"addGameText": "Tambah\nGame", "addGameText": "Tambah\nPermainan",
"cantOverwriteDefaultText": "Tidak dapat mengubah playlist semula!", "cantOverwriteDefaultText": "Tidak dapat mengubah daftar putar semula!",
"cantSaveAlreadyExistsText": "Playlist dengan nama ini sudah ada!", "cantSaveAlreadyExistsText": "Daftar Putar dengan nama ini sudah ada!",
"cantSaveEmptyListText": "Tidak dapat menyimpan playlist kosong!", "cantSaveEmptyListText": "Tidak dapat menyimpan daftar putar kosong!",
"editGameText": "Ubah\nPermainan", "editGameText": "Ubah\nPermainan",
"listNameText": "Nama Playlist", "listNameText": "Nama Daftar Putar",
"nameText": "Nama", "nameText": "Nama",
"removeGameText": "Hapus\nPermainan", "removeGameText": "Hapus\nPermainan",
"saveText": "Simpan Daftar", "saveText": "Simpan Daftar",
"titleText": "Pengaturan Playlist" "titleText": "Penyusun Daftar Putar"
}, },
"editProfileWindow": { "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)", "accountProfileText": "(Profil Akun)",
"availableText": "Nama ini \"${NAME}\" tersedia.", "availableText": "Nama ini \"${NAME}\" tersedia.",
"characterText": "Karakter", "characterText": "Karakter",
"checkingAvailabilityText": "Memeriksa Ketersediaan \"${NAME}\"...", "checkingAvailabilityText": "Memeriksa Ketersediaan \"${NAME}\"...",
"colorText": "warna", "colorText": "warna",
"getMoreCharactersText": "Dapatkan karakter lain...", "getMoreCharactersText": "Dapatkan karakter lain...",
"getMoreIconsText": "Dapatkan icon lain...", "getMoreIconsText": "Dapatkan ikon lain...",
"globalProfileInfoText": "profil pemain global dijamin untuk memiliki nama unik\ndi seluruh dunia. Termasuk juga icon lain.", "globalProfileInfoText": "profil pemain global dijamin untuk memiliki nama unik\ndi seluruh dunia. Termasuk juga ikon lain.",
"globalProfileText": "(Profil Global)", "globalProfileText": "(Profil Global)",
"highlightText": "highlight", "highlightText": "highlight",
"iconText": "icon", "iconText": "ikon",
"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", "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)", "localProfileText": "(Profil lokal)",
"nameDescriptionText": "Nama Pemain", "nameDescriptionText": "Nama Pemain",
"nameText": "Nama", "nameText": "Nama",
@ -588,7 +590,7 @@
"titleNewText": "Profil Baru", "titleNewText": "Profil Baru",
"unavailableText": "\"${NAME}\" tidak tersedia; coba nama lain.", "unavailableText": "\"${NAME}\" tidak tersedia; coba nama lain.",
"upgradeProfileInfoText": "Ini akan jadi nama Kamu dalam game ini\ndan memungkinkan Kamu untuk menetapkan ikon kustom.", "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": { "editSoundtrackWindow": {
"cantDeleteDefaultText": "Kamu tidak dapat menghapus soundtrack asal.", "cantDeleteDefaultText": "Kamu tidak dapat menghapus soundtrack asal.",
@ -608,12 +610,12 @@
"newSoundtrackNameText": "Soundtrack saya ${COUNT}", "newSoundtrackNameText": "Soundtrack saya ${COUNT}",
"newSoundtrackText": "Soundtrack Baru:", "newSoundtrackText": "Soundtrack Baru:",
"newText": "Buat\nSoundtrack", "newText": "Buat\nSoundtrack",
"selectAPlaylistText": "Pilih Playlist", "selectAPlaylistText": "Pilih Daftar Putar",
"selectASourceText": "Sumber Musik", "selectASourceText": "Sumber Musik",
"testText": "Test", "testText": "Tes",
"titleText": "Soundtrack", "titleText": "Soundtrack",
"useDefaultGameMusicText": "Musik Game Asal", "useDefaultGameMusicText": "Musik Game Asal",
"useITunesPlaylistText": "Music App Playlist", "useITunesPlaylistText": "Daftar Putar Apl. Musik",
"useMusicFileText": "Data Musik (mp3, dll)", "useMusicFileText": "Data Musik (mp3, dll)",
"useMusicFolderText": "berkas dari Data Musik" "useMusicFolderText": "berkas dari Data Musik"
}, },
@ -623,8 +625,10 @@
"epicDescriptionFilterText": "${DESCRIPTION} dalam slow-motion yang epik.", "epicDescriptionFilterText": "${DESCRIPTION} dalam slow-motion yang epik.",
"epicNameFilterText": "${NAME} Epik", "epicNameFilterText": "${NAME} Epik",
"errorAccessDeniedText": "akses ditolak", "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", "errorOutOfDiskSpaceText": "media penyimpanan tidak cukup",
"errorText": "Error", "errorSecureConnectionFailText": "Tidak bisa mendirikan koneksi aman; fungsi jaringan mungkin gagal.",
"errorText": "Kesalahan!",
"errorUnknownText": "kesalahan tak teridentifikasi", "errorUnknownText": "kesalahan tak teridentifikasi",
"exitGameText": "Keluar dari ${APP_NAME}?", "exitGameText": "Keluar dari ${APP_NAME}?",
"exportSuccessText": "'${NAME}' TEREXPORT", "exportSuccessText": "'${NAME}' TEREXPORT",
@ -663,7 +667,7 @@
"newText": "Buat\nPlaylist", "newText": "Buat\nPlaylist",
"showTutorialText": "Lihat Panduan", "showTutorialText": "Lihat Panduan",
"shuffleGameOrderText": "Acak Urutan Game", "shuffleGameOrderText": "Acak Urutan Game",
"titleText": "Ubah ${TYPE} Playlists" "titleText": "Ubah ${TYPE} Daftar Putar"
}, },
"gameSettingsWindow": { "gameSettingsWindow": {
"addGameText": "Tambah Game" "addGameText": "Tambah Game"
@ -786,7 +790,7 @@
"ticketPack4Text": "Paket Tiket Jumbo", "ticketPack4Text": "Paket Tiket Jumbo",
"ticketPack5Text": "Paket Tiket Raksasa", "ticketPack5Text": "Paket Tiket Raksasa",
"ticketPack6Text": "Paket Tiket Berlimpah", "ticketPack6Text": "Paket Tiket Berlimpah",
"ticketsFromASponsorText": "Dapatkan ${COUNT} tiket\ndari iklan", "ticketsFromASponsorText": "Tonton dapat\n${COUNT} tiket",
"ticketsText": "${COUNT} tiket", "ticketsText": "${COUNT} tiket",
"titleText": "Dapatkan 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.", "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" "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", "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", "googlePlayText": "Google Play",
"graphicsSettingsWindow": { "graphicsSettingsWindow": {
"alwaysText": "Selalu", "alwaysText": "Selalu",
@ -817,7 +822,7 @@
"visualsText": "Visual" "visualsText": "Visual"
}, },
"helpWindow": { "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!", "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.", "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.", "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", "powerupHealthNameText": "Kotak Medis",
"powerupIceBombsDescriptionText": "Lebih lemah dari bom biasa\ntapi membuat musuh Kamu beku,\npanik, gelisah, dan rapuh.", "powerupIceBombsDescriptionText": "Lebih lemah dari bom biasa\ntapi membuat musuh Kamu beku,\npanik, gelisah, dan rapuh.",
"powerupIceBombsNameText": "Bom Beku", "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", "powerupImpactBombsNameText": "Bom Pemicu",
"powerupLandMinesDescriptionText": "berisi 3 paket; berguna untuk\nbertahan atau menghentikan\nlangkah musuhmu.", "powerupLandMinesDescriptionText": "berisi 3 paket; berguna untuk\nbertahan atau menghentikan\nlangkah musuhmu.",
"powerupLandMinesNameText": "Ranjau", "powerupLandMinesNameText": "Ranjau",
@ -847,11 +852,11 @@
"powerupPunchNameText": "Sarung Tinju", "powerupPunchNameText": "Sarung Tinju",
"powerupShieldDescriptionText": "menahan beberapa serangan\nsehingga darah Kamu tidak berkurang.", "powerupShieldDescriptionText": "menahan beberapa serangan\nsehingga darah Kamu tidak berkurang.",
"powerupShieldNameText": "Energi Pelindung", "powerupShieldNameText": "Energi Pelindung",
"powerupStickyBombsDescriptionText": "lengket ke apapun yang tersentuh.\nSungguh Menjijikan..", "powerupStickyBombsDescriptionText": "Lengket ke apapun yang tersentuh.\nSungguh Menjijikkan..",
"powerupStickyBombsNameText": "Bom Lengket", "powerupStickyBombsNameText": "Bom Lengket",
"powerupsSubtitleText": "Jelas sekali, tidak ada game yang bakal seru tanpa Kekuatan Tambahan:", "powerupsSubtitleText": "Jelas sekali, tidak ada game yang bakal seru tanpa Kekuatan Tambahan:",
"powerupsText": "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.", "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.", "someDaysText": "Terkadang, kamu ingin sekali menghajar sesuatu atau menghancurkan sesuatu.",
"titleText": "Bantuan ${APP_NAME}", "titleText": "Bantuan ${APP_NAME}",
@ -1065,7 +1070,7 @@
"otherText": "Lainnya...", "otherText": "Lainnya...",
"outOfText": "(#${RANK} dari ${ALL})", "outOfText": "(#${RANK} dari ${ALL})",
"ownFlagAtYourBaseWarning": "Benderamu harus\nberada di basismu!", "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": { "partyWindow": {
"chatMessageText": "Pesan Obrolan", "chatMessageText": "Pesan Obrolan",
"emptyText": "acaramu kosong", "emptyText": "acaramu kosong",
@ -1110,7 +1115,10 @@
"playlistsText": "Daftar Putar", "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", "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...", "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", "pluginsText": "Plugin",
"practiceText": "Latihan", "practiceText": "Latihan",
"pressAnyButtonPlayAgainText": "Tekan tombol apa saja untuk kembali bermain...", "pressAnyButtonPlayAgainText": "Tekan tombol apa saja untuk kembali bermain...",
@ -1219,7 +1227,7 @@
"accountText": "Akun", "accountText": "Akun",
"advancedText": "Lanjutan", "advancedText": "Lanjutan",
"audioText": "Suara", "audioText": "Suara",
"controllersText": "pengontrol", "controllersText": "Pengontrol",
"graphicsText": "Grafik", "graphicsText": "Grafik",
"playerProfilesMovedText": "NB: Profil-Profil Pemain sudah dipindahkan di jendela Akun di menu utama.", "playerProfilesMovedText": "NB: Profil-Profil Pemain sudah dipindahkan di jendela Akun di menu utama.",
"playerProfilesText": "Profil Pemain", "playerProfilesText": "Profil Pemain",
@ -1236,7 +1244,7 @@
"enablePackageModsText": "Izinkan Paket Mod Lokal", "enablePackageModsText": "Izinkan Paket Mod Lokal",
"enterPromoCodeText": "Masukkan Kode", "enterPromoCodeText": "Masukkan Kode",
"forTestingText": "NB: jumlah ini hanya untuk tes dan akan hilang saat keluar", "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", "kickIdlePlayersText": "Keluarkan Pemain Diam",
"kidFriendlyModeText": "Mode Dibawah Umur (kekerasan rendah, dll)", "kidFriendlyModeText": "Mode Dibawah Umur (kekerasan rendah, dll)",
"languageText": "Bahasa", "languageText": "Bahasa",
@ -1252,7 +1260,7 @@
"translationFetchErrorText": "status translasi tidak tersedia.", "translationFetchErrorText": "status translasi tidak tersedia.",
"translationFetchingStatusText": "memeriksa status translasi", "translationFetchingStatusText": "memeriksa status translasi",
"translationInformMe": "Beritahu saya jika bahasa yang saya gunakan harus diperbarui", "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! **", "translationUpdateNeededText": "** bahasa ini perlu diperbaharui! **",
"vrTestingText": "Tes VR" "vrTestingText": "Tes VR"
}, },
@ -1301,7 +1309,7 @@
"holidaySpecialText": "Spesial Liburan", "holidaySpecialText": "Spesial Liburan",
"howToSwitchCharactersText": "pergi ke \"${SETTINGS} -> ${PLAYER_PROFILES}\" untuk mengubah karakter", "howToSwitchCharactersText": "pergi ke \"${SETTINGS} -> ${PLAYER_PROFILES}\" untuk mengubah karakter",
"howToUseIconsText": "(Buatlah profil pemain global (dalam jendela akun) untuk menggunakan ini)", "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", "iconsText": "Simbol",
"loadErrorText": "Tidak dapat memuat halaman.\nCek koneksi internetmu.", "loadErrorText": "Tidak dapat memuat halaman.\nCek koneksi internetmu.",
"loadingText": "memuat", "loadingText": "memuat",
@ -1362,6 +1370,7 @@
"tournamentStandingsText": "Hasil Terbaik Turnamen", "tournamentStandingsText": "Hasil Terbaik Turnamen",
"tournamentText": "Turnamen", "tournamentText": "Turnamen",
"tournamentTimeExpiredText": "Waktu Turnamen Berakhir", "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", "tournamentsText": "Turnamen",
"translations": { "translations": {
"characterNames": { "characterNames": {
@ -1416,23 +1425,23 @@
}, },
"gameDescriptions": { "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'.", "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 ${ARG1} seconds.": "Bawa bendera selama ${ARG1} detik.",
"Carry the flag for a set length of time.": "Bawa bendera dalam waktu yang ditentukan.", "Carry the flag for a set length of time.": "Bawa bendera dalam waktu yang ditentukan.",
"Crush ${ARG1} of your enemies.": "Hancurkan ${ARG1} musuh.", "Crush ${ARG1} of your enemies.": "Hancurkan ${ARG1} musuh.",
"Defeat all enemies.": "Hancurkan semua 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.", "Final glorious epic slow motion battle to the death.": "Pertarungan slow motion epik hingga kematian menjemput.",
"Gather eggs!": "Kumpulkan telur!", "Gather eggs!": "Kumpulkan telur!",
"Get the flag to the enemy end zone.": "Bawa bendera sampai ujung lapangan.", "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.", "Kill a set number of enemies to win.": "Hancurkan sejumlah musuh.",
"Last one standing wins.": "Terakhir hidup menang.", "Last one standing wins.": "Terakhir hidup menang.",
"Last remaining alive wins.": "Terakhir hidup menang.", "Last remaining alive wins.": "Terakhir hidup menang.",
"Last team standing wins.": "Habisi tim lawan.", "Last team standing wins.": "Habisi tim lawan.",
"Prevent enemies from reaching the exit.": "Tahan musuh jangan sampai finish.", "Prevent enemies from reaching the exit.": "Tahan musuh jangan sampai finish.",
"Reach the enemy flag to score.": "Sentuh bendera lawan untuk skor.", "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.": "Lari ${ARG1} putaran.",
"Run ${ARG1} laps. Your entire team has to finish.": "Lari ${ARG1} putaran. Seluruh tim harus mencapai finish.", "Run ${ARG1} laps. Your entire team has to finish.": "Lari ${ARG1} putaran. Seluruh tim harus mencapai finish.",
"Run 1 lap.": "Lari 1 putaran.", "Run 1 lap.": "Lari 1 putaran.",
@ -1691,7 +1700,7 @@
"Red": "Merah" "Red": "Merah"
}, },
"tips": { "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.", "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.", "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.", "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!", "winsPlayerText": "${NAME} Menang!",
"winsTeamText": "${NAME} Menang!", "winsTeamText": "${NAME} Menang!",
"winsText": "${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.", "worldScoresUnavailableText": "Skor Dunia tidak tersedia.",
"worldsBestScoresText": "Nilai Terbaik Dunia", "worldsBestScoresText": "Nilai Terbaik Dunia",
"worldsBestTimesText": "Waktu 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.", "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" "yourPowerRankingText": "La tua posizione assoluta"
}, },
"copyConfirmText": "Copiato negli appunti.",
"copyOfText": "${NAME} - Copia", "copyOfText": "${NAME} - Copia",
"copyText": "Copia", "copyText": "Copia",
"copyrightText": "© 2013 Eric Froemling", "copyrightText": "© 2013 Eric Froemling",
@ -650,7 +651,9 @@
"epicDescriptionFilterText": "${DESCRIPTION} a rallentatore leggendario.", "epicDescriptionFilterText": "${DESCRIPTION} a rallentatore leggendario.",
"epicNameFilterText": "${NAME} Leggendario", "epicNameFilterText": "${NAME} Leggendario",
"errorAccessDeniedText": "accesso negato", "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", "errorOutOfDiskSpaceText": "spazio su disco esaurito",
"errorSecureConnectionFailText": "Impossibile stabilire una connessione sicura con il cloud; le funzionalità online possono interrompersi.",
"errorText": "Errore", "errorText": "Errore",
"errorUnknownText": "errore sconosciuto", "errorUnknownText": "errore sconosciuto",
"exitGameText": "Uscire da ${APP_NAME}?", "exitGameText": "Uscire da ${APP_NAME}?",
@ -826,7 +829,7 @@
"ticketPack4Text": "Pacchetto Biglietti Jumbo", "ticketPack4Text": "Pacchetto Biglietti Jumbo",
"ticketPack5Text": "Pacchetto Biglietti Mammuth", "ticketPack5Text": "Pacchetto Biglietti Mammuth",
"ticketPack6Text": "Pacchetto Biglietti Ultimate", "ticketPack6Text": "Pacchetto Biglietti Ultimate",
"ticketsFromASponsorText": "Ottieni ${COUNT} biglietti\nda uno sponsor", "ticketsFromASponsorText": "Guarda una pubblicità\nper ${COUNT} biglietti",
"ticketsText": "${COUNT} Biglietti", "ticketsText": "${COUNT} Biglietti",
"titleText": "Ottieni 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ì.", "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" "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", "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", "googlePlayText": "Google Play",
"graphicsSettingsWindow": { "graphicsSettingsWindow": {
"alwaysText": "Sempre", "alwaysText": "Sempre",
@ -1427,6 +1431,7 @@
"tournamentStandingsText": "Classifica del torneo", "tournamentStandingsText": "Classifica del torneo",
"tournamentText": "Torneo", "tournamentText": "Torneo",
"tournamentTimeExpiredText": "Tempo del torneo esaurito", "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", "tournamentsText": "Tornei",
"translations": { "translations": {
"characterNames": { "characterNames": {

View file

@ -327,6 +327,7 @@
"achievementsRemainingText": "남은 업적:", "achievementsRemainingText": "남은 업적:",
"achievementsText": "업적", "achievementsText": "업적",
"achievementsUnavailableForOldSeasonsText": "죄송합니다만 이전 시즌에 대해서는 업적 정보가 제공되지 않습니다.", "achievementsUnavailableForOldSeasonsText": "죄송합니다만 이전 시즌에 대해서는 업적 정보가 제공되지 않습니다.",
"activatedText": "${THING}가 작동을 시작했습니다.",
"addGameWindow": { "addGameWindow": {
"getMoreGamesText": "다른 게임 보기...", "getMoreGamesText": "다른 게임 보기...",
"titleText": "게임 추가" "titleText": "게임 추가"
@ -624,7 +625,9 @@
"epicDescriptionFilterText": "(에픽 슬로 모션) ${DESCRIPTION}.", "epicDescriptionFilterText": "(에픽 슬로 모션) ${DESCRIPTION}.",
"epicNameFilterText": "에픽 ${NAME}", "epicNameFilterText": "에픽 ${NAME}",
"errorAccessDeniedText": "액세스가 거부됨", "errorAccessDeniedText": "액세스가 거부됨",
"errorDeviceTimeIncorrectText": "당신의 디바이스 시간은 ${HOURS}시간이나 맞지 않습니다.\n이러면 아마 문제를 불러 이르킬 수 있습니다.\n디바이스의 시간을 현재 시각으로 바꿔 주십시오.",
"errorOutOfDiskSpaceText": "디스크 공간 부족", "errorOutOfDiskSpaceText": "디스크 공간 부족",
"errorSecureConnectionFailText": "클라우드 연결 상황이 안전하지 않습니다. 네트워크가 종종 연결 해제 될 수 있습니다.",
"errorText": "오류", "errorText": "오류",
"errorUnknownText": "알 수 없는 오류", "errorUnknownText": "알 수 없는 오류",
"exitGameText": "${APP_NAME}를 종료하시겠습니까?", "exitGameText": "${APP_NAME}를 종료하시겠습니까?",
@ -787,7 +790,7 @@
"ticketPack4Text": "점보 티켓 팩", "ticketPack4Text": "점보 티켓 팩",
"ticketPack5Text": "매머드 티켓 팩", "ticketPack5Text": "매머드 티켓 팩",
"ticketPack6Text": "궁극의 티켓 팩", "ticketPack6Text": "궁극의 티켓 팩",
"ticketsFromASponsorText": "스폰서로부터 티켓\n${COUNT}장 받기", "ticketsFromASponsorText": "광고 보고\n티켓 ${COUNT}장 받기",
"ticketsText": "티켓 ${COUNT}장", "ticketsText": "티켓 ${COUNT}장",
"titleText": "티켓 구입", "titleText": "티켓 구입",
"unavailableLinkAccountText": "죄송합니다만 이 플랫폼에서는 구매할 수 없습니다.\n해결책으로, 이 계정을 다른 플랫폼의 계정에 연동하여\n그곳에서 구매를 진행할 수 있습니다.", "unavailableLinkAccountText": "죄송합니다만 이 플랫폼에서는 구매할 수 없습니다.\n해결책으로, 이 계정을 다른 플랫폼의 계정에 연동하여\n그곳에서 구매를 진행할 수 있습니다.",
@ -798,6 +801,7 @@
"youHaveText": "보유량: ${COUNT} 티켓" "youHaveText": "보유량: ${COUNT} 티켓"
}, },
"googleMultiplayerDiscontinuedText": "죄송하지만, 구글의 멀티플레이어 서비스는 더이상 이용할수가 없어요.\n지금 대체제에 가능한 빨리 작업중이에요.\n그 때까지는, 다른 접속 방법을 사용해주세요.\n-Eric", "googleMultiplayerDiscontinuedText": "죄송하지만, 구글의 멀티플레이어 서비스는 더이상 이용할수가 없어요.\n지금 대체제에 가능한 빨리 작업중이에요.\n그 때까지는, 다른 접속 방법을 사용해주세요.\n-Eric",
"googlePlayPurchasesNotAvailableText": "구매가 되지 않았습니다.\n아마 스토어 앱을 업데이트 해야 합니다.",
"googlePlayText": "Google Play", "googlePlayText": "Google Play",
"graphicsSettingsWindow": { "graphicsSettingsWindow": {
"alwaysText": "언제나", "alwaysText": "언제나",
@ -1108,7 +1112,10 @@
"playlistsText": "플레이 목록", "playlistsText": "플레이 목록",
"pleaseRateText": "${APP_NAME} 앱이 마음에 드시면 잠시 시간을 내어\n평가를 하거나 리뷰를 남겨주세요. 저희가 유용한\n피드백을 얻을 수 있고 향후 개발에 도움이 됩니다.\n\n감사합니다!\n-eric", "pleaseRateText": "${APP_NAME} 앱이 마음에 드시면 잠시 시간을 내어\n평가를 하거나 리뷰를 남겨주세요. 저희가 유용한\n피드백을 얻을 수 있고 향후 개발에 도움이 됩니다.\n\n감사합니다!\n-eric",
"pleaseWaitText": "잠시만 기다려 주십시오...", "pleaseWaitText": "잠시만 기다려 주십시오...",
"pluginsDetectedText": "새로운 플러그인 감지됨. 설정에서 활성/설정해 주십시오.", "pluginClassLoadErrorText": "플러그인(${PLUGIN})을 불러오는 도중에 오류가 생겼습니다. 오류 : ${ERROR}",
"pluginInitErrorText": "플러그인(${PLUGIN})을 실행하는 도중에 오류가 생겼습니다. 오류 : ${ERROR}",
"pluginsDetectedText": "새로운 플러그인이 감지되었습니다. 게임을 재시작 하거나 설정을 바꿔 주십시오.",
"pluginsRemovedText": "${NUM} 플러그인을 더 이상 찾을 수 없습니다.",
"pluginsText": "플러그인", "pluginsText": "플러그인",
"practiceText": "연습", "practiceText": "연습",
"pressAnyButtonPlayAgainText": "다시 플레이하려면 아무 버튼이나 누르세요...", "pressAnyButtonPlayAgainText": "다시 플레이하려면 아무 버튼이나 누르세요...",
@ -1360,6 +1367,7 @@
"tournamentStandingsText": "토너먼트 성적", "tournamentStandingsText": "토너먼트 성적",
"tournamentText": "토너먼트", "tournamentText": "토너먼트",
"tournamentTimeExpiredText": "토너먼트 시간이 종료되었습니다", "tournamentTimeExpiredText": "토너먼트 시간이 종료되었습니다",
"tournamentsDisabledWorkspaceText": "워크숍이 가동 중이라 토너먼트를 할 수 없습니다.\n토너먼트를 하려면, 워크숍을 끄고 게임을 재시작 하시오.",
"tournamentsText": "토너먼트", "tournamentsText": "토너먼트",
"translations": { "translations": {
"characterNames": { "characterNames": {
@ -1844,6 +1852,8 @@
"winsPlayerText": "${NAME} 님 승리!", "winsPlayerText": "${NAME} 님 승리!",
"winsTeamText": "${NAME} 팀 승리!", "winsTeamText": "${NAME} 팀 승리!",
"winsText": "${NAME} 님 승리!", "winsText": "${NAME} 님 승리!",
"workspaceSyncErrorText": "${WORKSPACE}를 동기화하다가 오류가 났습니다. 로그를 확인하세요.",
"workspaceSyncReuseText": "${WORKSPACE}를 동기화 할 수 없습니다. 마지막으로 동기화 된 버전으로 돌아갑니다.",
"worldScoresUnavailableText": "세계 기록을 이용할 수 없습니다.", "worldScoresUnavailableText": "세계 기록을 이용할 수 없습니다.",
"worldsBestScoresText": "세계 최고 점수", "worldsBestScoresText": "세계 최고 점수",
"worldsBestTimesText": "세계 최고 시간", "worldsBestTimesText": "세계 최고 시간",

View file

@ -499,6 +499,7 @@
"welcome2Text": "همچنین می‌توانید از راه‌های مشابه بلیت جمع‌آوری کنید.\nبلیتها می‌توانند برای باز کردن بازیکنان جدید، نقشه‌ها، مینی‌بازی‌ها یا برای ورود در مسابقه‌ها و موارد\nبیشتر مورد استفاده قرار گیرند.", "welcome2Text": "همچنین می‌توانید از راه‌های مشابه بلیت جمع‌آوری کنید.\nبلیتها می‌توانند برای باز کردن بازیکنان جدید، نقشه‌ها، مینی‌بازی‌ها یا برای ورود در مسابقه‌ها و موارد\nبیشتر مورد استفاده قرار گیرند.",
"yourPowerRankingText": "رتبه‌بندی قدرت شما:" "yourPowerRankingText": "رتبه‌بندی قدرت شما:"
}, },
"copyConfirmText": "در حافظه کلیپ بورد شما کپی شد.",
"copyOfText": "${NAME} کپی", "copyOfText": "${NAME} کپی",
"copyText": "کپی کردن", "copyText": "کپی کردن",
"createEditPlayerText": "<ایجاد/ویرایش بازیکن>", "createEditPlayerText": "<ایجاد/ویرایش بازیکن>",
@ -626,7 +627,7 @@
"epicDescriptionFilterText": "در حماسهٔ حرکت آهسته ${DESCRIPTION}", "epicDescriptionFilterText": "در حماسهٔ حرکت آهسته ${DESCRIPTION}",
"epicNameFilterText": "${NAME} حماسهٔ", "epicNameFilterText": "${NAME} حماسهٔ",
"errorAccessDeniedText": "دسترسی رد شد", "errorAccessDeniedText": "دسترسی رد شد",
"errorDeviceTimeIncorrectText": "ساعت گوشی ${HOURS} ساعت خاموش بوده‌است.\nممکن است مشکل به‌وجود بیاید.\nلطفاً ساعت و منطقهٔ زمانی گوشی‌تان را بررسی کنید.", "errorDeviceTimeIncorrectText": "ساعت گوشی‌تان ${HOURS} ساعت خطا دارد.\nممکن است مشکل به‌وجود بیاید.\nلطفاً ساعت و منطقه زمانی گوشی‌تان را بررسی کنید.",
"errorOutOfDiskSpaceText": "حافظه جا ندارد", "errorOutOfDiskSpaceText": "حافظه جا ندارد",
"errorSecureConnectionFailText": "قادر به ایجاد اتصال ابری امن نیست. عملکرد شبکه ممکن است خراب شود.", "errorSecureConnectionFailText": "قادر به ایجاد اتصال ابری امن نیست. عملکرد شبکه ممکن است خراب شود.",
"errorText": "خطا", "errorText": "خطا",
@ -802,6 +803,7 @@
"youHaveText": ".بلیط دارید ${COUNT} شما" "youHaveText": ".بلیط دارید ${COUNT} شما"
}, },
"googleMultiplayerDiscontinuedText": "متأسفیم ، سرویس چند نفره Google دیگر در دسترس نیست.\nمن در اسرع وقت در حال جایگزینی هستم.\nتا آن زمان ، لطفاً روش اتصال دیگری را امتحان کنید.", "googleMultiplayerDiscontinuedText": "متأسفیم ، سرویس چند نفره Google دیگر در دسترس نیست.\nمن در اسرع وقت در حال جایگزینی هستم.\nتا آن زمان ، لطفاً روش اتصال دیگری را امتحان کنید.",
"googlePlayPurchasesNotAvailableText": "خرید های گوگل‌پلی در دسترس نیستند.\nاحتمالا باید برنامه‌ی استور خود را بروز‌رسانی کنید.",
"googlePlayText": "گوگل پلی", "googlePlayText": "گوگل پلی",
"graphicsSettingsWindow": { "graphicsSettingsWindow": {
"alwaysText": "همیشه", "alwaysText": "همیشه",
@ -844,8 +846,8 @@
"powerupHealthNameText": "جعبه درمان", "powerupHealthNameText": "جعبه درمان",
"powerupIceBombsDescriptionText": "با این بمب های یخی میتونید حریف ها\nرو منجمد و آسیب پذیر کنید ولی بهتر خودتون\nتوی نزدیکی انفجار این بمب ها قرار نگیرین", "powerupIceBombsDescriptionText": "با این بمب های یخی میتونید حریف ها\nرو منجمد و آسیب پذیر کنید ولی بهتر خودتون\nتوی نزدیکی انفجار این بمب ها قرار نگیرین",
"powerupIceBombsNameText": "بمب یخی", "powerupIceBombsNameText": "بمب یخی",
"powerupImpactBombsDescriptionText": "بهش میگن بمب ببر تا به چیزی برخورد\nنکنن منفجر نمیشن", "powerupImpactBombsDescriptionText": "بهش میگن بمب فعال‌شونده تا به چیزی برخورد\nنکنن منفجر نمیشن",
"powerupImpactBombsNameText": "بمب ببر", "powerupImpactBombsNameText": "بمب فعال‌شونده",
"powerupLandMinesDescriptionText": "با این جعبه به شما سه تا مین\nداده میشه که تا وقتی پرتاب بشن\nتا چیزی روشون میخوره منفجر میشن", "powerupLandMinesDescriptionText": "با این جعبه به شما سه تا مین\nداده میشه که تا وقتی پرتاب بشن\nتا چیزی روشون میخوره منفجر میشن",
"powerupLandMinesNameText": "مین زمینی", "powerupLandMinesNameText": "مین زمینی",
"powerupPunchDescriptionText": "بهتون دستکش بکس میده و باعث\nمیشه ضربه مشت های قویتری داشته باشید", "powerupPunchDescriptionText": "بهتون دستکش بکس میده و باعث\nمیشه ضربه مشت های قویتری داشته باشید",
@ -1367,6 +1369,7 @@
"tournamentStandingsText": "جدول رده بندی مسابقات", "tournamentStandingsText": "جدول رده بندی مسابقات",
"tournamentText": "جام حذفی", "tournamentText": "جام حذفی",
"tournamentTimeExpiredText": "زمان مسابقات پایان یافت", "tournamentTimeExpiredText": "زمان مسابقات پایان یافت",
"tournamentsDisabledWorkspaceText": "وقتی فضاهای کاری فعال هستند، مسابقات غیرفعال می شوند.\n برای فعال کردن مجدد مسابقات، فضای کاری خود را غیرفعال کنید و دوباره راه اندازی کنید.",
"tournamentsText": "مسابقات", "tournamentsText": "مسابقات",
"translations": { "translations": {
"characterNames": { "characterNames": {
@ -1375,11 +1378,11 @@
"Bernard": "برنارد", "Bernard": "برنارد",
"Bones": "اسکلت", "Bones": "اسکلت",
"Butch": "بوچ", "Butch": "بوچ",
"Easter Bunny": "Easter خرگوش جشن", "Easter Bunny": "خرگوش عید پاک",
"Flopsy": "فلاپسی", "Flopsy": "فلاپسی",
"Frosty": "آدم‌برفی", "Frosty": "آدم‌برفی",
"Gretel": "گرتل", "Gretel": "گرتل",
"Grumbledorf": "تردست", "Grumbledorf": "جادوگر",
"Jack Morgan": "جک مورگان", "Jack Morgan": "جک مورگان",
"Kronk": "کرانک", "Kronk": "کرانک",
"Lee": "لی", "Lee": "لی",
@ -1387,12 +1390,12 @@
"Mel": "مل", "Mel": "مل",
"Middle-Man": "کودن", "Middle-Man": "کودن",
"Minimus": "مینیموس", "Minimus": "مینیموس",
"Pascal": نگوئن", "Pascal": اسکال",
"Pixel": "فرشته", "Pixel": "پیکسل",
"Sammy Slam": "سامی کشتی‌گیر", "Sammy Slam": "سامی کشتی‌گیر",
"Santa Claus": "بابا نوئل", "Santa Claus": "بابا نوئل",
"Snake Shadow": "سایه ی مار", "Snake Shadow": "سایه ی مار",
"Spaz": "فلج", "Spaz": "اسپاز",
"Taobao Mascot": "تائوبائو", "Taobao Mascot": "تائوبائو",
"Todd": "تاد", "Todd": "تاد",
"Todd McBurton": "تاد", "Todd McBurton": "تاد",
@ -1590,7 +1593,7 @@
"Can't link 2 accounts of this type.": ".نمی‌توان دو حساب از این نوع را پیوند داد", "Can't link 2 accounts of this type.": ".نمی‌توان دو حساب از این نوع را پیوند داد",
"Can't link 2 diamond league accounts.": "نمی‌توان حساب دو لیگ الماس را پیوند داد.", "Can't link 2 diamond league accounts.": "نمی‌توان حساب دو لیگ الماس را پیوند داد.",
"Can't link; would surpass maximum of ${COUNT} linked accounts.": "پیوند نمی‌شود; حداکثر از ${COUNT} پیوند پشتیبانی می‌شود.", "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.": "نمیتوان یک اتصال امن ایجاد کرد", "Could not establish a secure connection.": "نمیتوان یک اتصال امن ایجاد کرد",
"Daily maximum reached.": "به حداکثر روزانه رسیده است", "Daily maximum reached.": "به حداکثر روزانه رسیده است",
"Entering tournament...": "ورود به مسابقات ...", "Entering tournament...": "ورود به مسابقات ...",
@ -1774,14 +1777,14 @@
"phrase24Text": "باریکلا ! به این میگن انفجار", "phrase24Text": "باریکلا ! به این میگن انفجار",
"phrase25Text": "دیدی اصلا سخت نبود ؟", "phrase25Text": "دیدی اصلا سخت نبود ؟",
"phrase26Text": "حالا دیگه مثل یه ببر قوی شدی", "phrase26Text": "حالا دیگه مثل یه ببر قوی شدی",
"phrase27Text": "دشمنا منتظرت هستن ... پس این آموزش ها رو به خاطر بسپار", "phrase27Text": "این آموزش ها رو به یاد داشته باش، و مطمئن باش که زنده برمی‌گردی!",
"phrase28Text": "! ببینم چند مرده حلاجی پهلوون", "phrase28Text": "! ببینم چند مرده حلاجی پهلوون",
"phrase29Text": "! خدا قوت", "phrase29Text": "! خدا قوت",
"randomName1Text": "شایان", "randomName1Text": "فرد",
"randomName2Text": "بهنام", "randomName2Text": "هری",
"randomName3Text": "کاوه", "randomName3Text": "بیل",
"randomName4Text": "مهدی", "randomName4Text": "چاک",
"randomName5Text": "بهرام", "randomName5Text": "فیل",
"skipConfirmText": ".واقعا از آموزش رد می‌شی ؟ هر کلیدی رو بزن تا رد بشیم", "skipConfirmText": ".واقعا از آموزش رد می‌شی ؟ هر کلیدی رو بزن تا رد بشیم",
"skipVoteCountText": "نفر خواستار رد شدن از آموزش هستند ${TOTAL} نفر از ${COUNT}", "skipVoteCountText": "نفر خواستار رد شدن از آموزش هستند ${TOTAL} نفر از ${COUNT}",
"skippingText": "از آموزش می گذریم", "skippingText": "از آموزش می گذریم",

View file

@ -331,6 +331,7 @@
"achievementsRemainingText": "Pozostałe Osiągnięcia:", "achievementsRemainingText": "Pozostałe Osiągnięcia:",
"achievementsText": "Osiągnięcia", "achievementsText": "Osiągnięcia",
"achievementsUnavailableForOldSeasonsText": "Wybacz, lecz szczegóły osiągnięć nie są dostępne dla starych sezonów.", "achievementsUnavailableForOldSeasonsText": "Wybacz, lecz szczegóły osiągnięć nie są dostępne dla starych sezonów.",
"activatedText": "${THING} aktywowane/a",
"addGameWindow": { "addGameWindow": {
"getMoreGamesText": "Więcej rozgrywek...", "getMoreGamesText": "Więcej rozgrywek...",
"titleText": "Dodaj grę" "titleText": "Dodaj grę"
@ -647,7 +648,9 @@
"epicDescriptionFilterText": "${DESCRIPTION} Epickie zwolnione tempo.", "epicDescriptionFilterText": "${DESCRIPTION} Epickie zwolnione tempo.",
"epicNameFilterText": "Epicki tryb - ${NAME}", "epicNameFilterText": "Epicki tryb - ${NAME}",
"errorAccessDeniedText": "odmowa dostępu", "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", "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", "errorText": "Błąd",
"errorUnknownText": "nieznany błąd", "errorUnknownText": "nieznany błąd",
"exitGameText": "Wyjść z ${APP_NAME}?", "exitGameText": "Wyjść z ${APP_NAME}?",
@ -825,7 +828,7 @@
"ticketPack4Text": "Paczka Kolos kuponów", "ticketPack4Text": "Paczka Kolos kuponów",
"ticketPack5Text": "Mamucia paczka kuponów", "ticketPack5Text": "Mamucia paczka kuponów",
"ticketPack6Text": "Paczka Ultimate 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", "ticketsText": "${COUNT} kuponów",
"titleText": "Zdobądź kupony", "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.", "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" "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", "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", "googlePlayText": "Google Play",
"graphicsSettingsWindow": { "graphicsSettingsWindow": {
"alwaysText": "Zawsze", "alwaysText": "Zawsze",
@ -1161,7 +1165,10 @@
"playlistsText": "Listy gier", "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", "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ę...", "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.", "pluginsDetectedText": "Wykryto nowe pluginy. Uruchom ponownie grę, aby je aktywować, lub skonfiguruje je w ustawieniach.",
"pluginsRemovedText": "Usunięto ${NUM} pluginy(ów)",
"pluginsText": "Pluginy", "pluginsText": "Pluginy",
"practiceText": "Praktyka", "practiceText": "Praktyka",
"pressAnyButtonPlayAgainText": "Naciśnij dowolny przycisk aby zagrać ponownie...", "pressAnyButtonPlayAgainText": "Naciśnij dowolny przycisk aby zagrać ponownie...",
@ -1425,6 +1432,7 @@
"tournamentStandingsText": "Klasyfikacja Turnieju", "tournamentStandingsText": "Klasyfikacja Turnieju",
"tournamentText": "Turniej", "tournamentText": "Turniej",
"tournamentTimeExpiredText": "Czas Turnieju wygasł", "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", "tournamentsText": "Turnieje",
"translations": { "translations": {
"characterNames": { "characterNames": {
@ -1937,6 +1945,8 @@
"winsPlayerText": "${NAME} Wygrywa!", "winsPlayerText": "${NAME} Wygrywa!",
"winsTeamText": "${NAME} Wygrywają!", "winsTeamText": "${NAME} Wygrywają!",
"winsText": "${NAME} Wygrywa!", "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.", "worldScoresUnavailableText": "Ogólnoświatowe wyniki niedostępne.",
"worldsBestScoresText": "Najlepsze ogólnoświatowe wyniki", "worldsBestScoresText": "Najlepsze ogólnoświatowe wyniki",
"worldsBestTimesText": "Najlepsze ogólnoświatowe czasy", "worldsBestTimesText": "Najlepsze ogólnoświatowe czasy",

View file

@ -658,7 +658,7 @@
"epicDescriptionFilterText": "${DESCRIPTION} em câmera lenta épica.", "epicDescriptionFilterText": "${DESCRIPTION} em câmera lenta épica.",
"epicNameFilterText": "${NAME} épico(a)", "epicNameFilterText": "${NAME} épico(a)",
"errorAccessDeniedText": "acesso negado", "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", "errorOutOfDiskSpaceText": "pouco espaço em disco",
"errorSecureConnectionFailText": "Não foi possível estabelecer uma conexão segura à nuvem; a funcionalidade da rede pode falhar.", "errorSecureConnectionFailText": "Não foi possível estabelecer uma conexão segura à nuvem; a funcionalidade da rede pode falhar.",
"errorText": "Erro", "errorText": "Erro",
@ -1458,6 +1458,7 @@
"tournamentStandingsText": "Classificação do torneio", "tournamentStandingsText": "Classificação do torneio",
"tournamentText": "Torneio", "tournamentText": "Torneio",
"tournamentTimeExpiredText": "O tempo do torneio expirou.", "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", "tournamentsText": "Torneios",
"translations": { "translations": {
"characterNames": { "characterNames": {

View file

@ -1,28 +1,28 @@
{ {
"accountSettingsWindow": { "accountSettingsWindow": {
"accountNameRules": "Имена учетных записей не могут содержать эмодзи или другие специальные символы", "accountNameRules": "Имена аккаунтов не могут содержать эмодзи или другие специальные символы",
"accountProfileText": "(профиль)", "accountProfileText": "(профиль)",
"accountsText": "Аккаунты", "accountsText": "Аккаунты",
"achievementProgressText": "Достижения: ${COUNT} из ${TOTAL}", "achievementProgressText": "Достижения: ${COUNT} из ${TOTAL}",
"campaignProgressText": "Прогресс кампании [Сложный режим]: ${PROGRESS}", "campaignProgressText": "Прогресс кампании [Сложный режим]: ${PROGRESS}",
"changeOncePerSeason": "Вы можете изменить это только раз в сезон.", "changeOncePerSeason": "Вы можете изменить это только раз в сезон.",
"changeOncePerSeasonError": "Вы должны подождать до следующего сезона, чтобы изменить это снова (${NUM} дней)", "changeOncePerSeasonError": "Вы должны подождать до следующего сезона, чтобы изменить это снова (${NUM} дней)",
"customName": "Имя учётной записи", "customName": "Имя аккаунта",
"deviceSpecificAccountText": "Сейчас используется аккаунт имениустройства: ${NAME}", "deviceSpecificAccountText": "Сейчас используется аккаунт имениустройства: ${NAME}",
"linkAccountsEnterCodeText": "Введите код", "linkAccountsEnterCodeText": "Введите код",
"linkAccountsGenerateCodeText": "Сгенерировать код", "linkAccountsGenerateCodeText": "Сгенерировать код",
"linkAccountsInfoText": "(делиться достижениями с другими платформами)", "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} аккаунтов.", "linkAccountsInstructionsText": "Для связки двух аккаунтов, создайте код на одном\nиз них и введите код на другом.\nПрогресс и инвентарь будут объединены.\nВы можете связать до ${COUNT} аккаунтов.",
"linkAccountsText": "Связать акаунты", "linkAccountsText": "Связать акаунты",
"linkedAccountsText": "Привязанные аккаунты:", "linkedAccountsText": "Привязанные аккаунты:",
"nameChangeConfirm": "Вы уверены, что хотите сменить имя аккаунта на ${NAME}?", "nameChangeConfirm": "Вы уверены, что хотите сменить имя аккаунта на ${NAME}?",
"notLoggedInText": "<не авторизован>", "notLoggedInText": "<не авторизован>",
"resetProgressConfirmNoAchievementsText": "Это сбросит весь ваш кооперативный прогресс\nи локальные лучшие результаты (кроме билетов).\nЭтот процесс необратим. Вы уверены?", "resetProgressConfirmNoAchievementsText": "Это сбросит весь ваш кооперативный прогресс\nи локальные рекорды (но не билеты).\nЭтот процесс необратим. Вы уверены?",
"resetProgressConfirmText": "Это сбросит весь ваш кооперативный\nпрогресс, достижения и локальные результаты\n(кроме билетов). Этот процесс необратим.\nВы уверены?", "resetProgressConfirmText": "Это сбросит весь ваш кооперативный\nпрогресс, достижения и локальные рекорды\n(кроме билетов). Этот процесс необратим.\nВы уверены?",
"resetProgressText": "Сбросить прогресс", "resetProgressText": "Сбросить прогресс",
"setAccountName": "Задать имя аккаунта", "setAccountName": "Задать имя аккаунта",
"setAccountNameDesc": "Выберите имя для отображения своей учетной записи.\nВы можете использовать имя одной из ваших связанных\nучетных записей или создать уникальное имя учётной записи.", "setAccountNameDesc": "Выберите имя для отображения своего аккаунта.\nВы можете использовать имя одного из ваших связанных аккаунтов или создать уникальное имя аккаунта.",
"signInInfoText": "Войдите в аккаунт, чтобы собирать билеты, \nсоревноваться онлайн и делиться успехами.", "signInInfoText": "Войдите в аккаунт, чтобы собирать билеты, \nсоревноваться онлайн и делиться успехами.",
"signInText": "Войти", "signInText": "Войти",
"signInWithDeviceInfoText": "(стандартный аккаунт только для этого устройства)", "signInWithDeviceInfoText": "(стандартный аккаунт только для этого устройства)",
@ -52,11 +52,11 @@
"achievementText": "Достижение", "achievementText": "Достижение",
"achievements": { "achievements": {
"Boom Goes the Dynamite": { "Boom Goes the Dynamite": {
"description": "Убейте 3 плохих парней с помощью TNT", "description": "Убейте 3 негодяев с помощью TNT",
"descriptionComplete": "С помощью TNT убито 3 плохих парней", "descriptionComplete": "С помощью TNT убито 3 негодяев",
"descriptionFull": "Убейте 3 плохих парней с помощью TNT на уровне ${LEVEL}", "descriptionFull": "Убейте 3 негодяев с помощью TNT на уровне ${LEVEL}",
"descriptionFullComplete": "3 плохих парней убито с помощью TNT на уровне ${LEVEL}", "descriptionFullComplete": "3 негодяя убито с помощью TNT на уровне ${LEVEL}",
"name": "Динамит сейчас взорвётся!" "name": "Динамит делает “БУМ”!"
}, },
"Boxer": { "Boxer": {
"description": "Победите без использования бомб", "description": "Победите без использования бомб",
@ -68,26 +68,26 @@
"Dual Wielding": { "Dual Wielding": {
"descriptionFull": "Соединить 2 контроллера (аппарат или приложение)", "descriptionFull": "Соединить 2 контроллера (аппарат или приложение)",
"descriptionFullComplete": "Соединено 2 контроллера (аппарат или приложение)", "descriptionFullComplete": "Соединено 2 контроллера (аппарат или приложение)",
"name": "Двойное оружие" "name": "Пáрное оружие"
}, },
"Flawless Victory": { "Flawless Victory": {
"description": "Победите не получив повреждений", "description": "Победите не получив урона",
"descriptionComplete": "Победа без повреждений", "descriptionComplete": "Победа без получения урона",
"descriptionFull": "Пройдите уровень ${LEVEL} не получив повреждений", "descriptionFull": "Пройдите уровень ${LEVEL} не получив урона",
"descriptionFullComplete": "Уровень ${LEVEL} пройден без повреждений", "descriptionFullComplete": "Уровень ${LEVEL} пройден без урона",
"name": "Безупречная победа" "name": "Чистая победа"
}, },
"Free Loader": { "Free Loader": {
"descriptionFull": "Начать игру каждый сам за себя с 2 и более игроками", "descriptionFull": "Начать игру “Каждый сам за себя” с 2 и более игроками",
"descriptionFullComplete": "Начата игра каждый сам за себя с 2 и более игроками", "descriptionFullComplete": "Начата игра “Каждый сам за себя” с 2 и более игроками",
"name": "Один в поле воин" "name": "Один в поле воин"
}, },
"Gold Miner": { "Gold Miner": {
"description": "Убейте 6 плохих парней с помощью мин", "description": "Убейте 6 негодяев с помощью мин",
"descriptionComplete": "С помощью мин убито 6 плохих парней", "descriptionComplete": "С помощью мин убито 6 негодяев",
"descriptionFull": "Убейте 6 плохих парней с помощью мин на уровне ${LEVEL}", "descriptionFull": "Убейте 6 негодяев с помощью мин на уровне ${LEVEL}",
"descriptionFullComplete": "6 плохих парней убито с помощью мин на уровне ${LEVEL}", "descriptionFullComplete": "6 негодяев убито с помощью мин на уровне ${LEVEL}",
"name": "Золотой минёр" "name": "Сапер-чемпион"
}, },
"Got the Moves": { "Got the Moves": {
"description": "Победите без ударов и бомб", "description": "Победите без ударов и бомб",
@ -124,8 +124,8 @@
}, },
"Mine Games": { "Mine Games": {
"description": "Убейте 3 плохих парней с помощью мин", "description": "Убейте 3 плохих парней с помощью мин",
"descriptionComplete": "С помощью мин убито 3 злодея", "descriptionComplete": "С помощью мин убито 3 негодяя",
"descriptionFull": "Убейте 3 плохих парней с помощью мин на уровне ${LEVEL}", "descriptionFull": "Убейте 3 негодяев с помощью мин на уровне ${LEVEL}",
"descriptionFullComplete": "С помощью мин убито 3 негодяя на уровне ${LEVEL}", "descriptionFullComplete": "С помощью мин убито 3 негодяя на уровне ${LEVEL}",
"name": "Игры с минами" "name": "Игры с минами"
}, },
@ -176,14 +176,14 @@
"descriptionComplete": "Победа без использования бомб", "descriptionComplete": "Победа без использования бомб",
"descriptionFull": "Пройдите уровень ${LEVEL} без использования бомб", "descriptionFull": "Пройдите уровень ${LEVEL} без использования бомб",
"descriptionFullComplete": "Уровень ${LEVEL} пройден без использования бомб", "descriptionFullComplete": "Уровень ${LEVEL} пройден без использования бомб",
"name": "Боксёр профи" "name": "Боксёр-профи"
}, },
"Pro Football Shutout": { "Pro Football Shutout": {
"description": "Победите в сухую", "description": "Победите всухую",
"descriptionComplete": "Уровень был пройден в сухую", "descriptionComplete": "Уровень был пройден всухую",
"descriptionFull": "Выиграйте матч ${LEVEL} в сухую", "descriptionFull": "Выиграйте матч ${LEVEL} всухую",
"descriptionFullComplete": "Победа в матче ${LEVEL} в сухую", "descriptionFullComplete": "Победа в матче ${LEVEL} всухую",
"name": "${LEVEL} в сухую" "name": "${LEVEL} всухую"
}, },
"Pro Football Victory": { "Pro Football Victory": {
"description": "Выиграйте матч", "description": "Выиграйте матч",
@ -207,11 +207,11 @@
"name": "Победа на уровне ${LEVEL}" "name": "Победа на уровне ${LEVEL}"
}, },
"Rookie Football Shutout": { "Rookie Football Shutout": {
"description": "Выиграйте, не дав злодеям забить", "description": "Выиграйте всухую",
"descriptionComplete": "Победа в сухую", "descriptionComplete": "Победа всухую",
"descriptionFull": "Выиграйте матч ${LEVEL}, не дав злодеям забить", "descriptionFull": "Выиграйте матч ${LEVEL}, всухую",
"descriptionFullComplete": "Победа в матче ${LEVEL} в сухую", "descriptionFullComplete": "Победа в матче ${LEVEL} всухую",
"name": "${LEVEL} в сухую" "name": "${LEVEL} всухую"
}, },
"Rookie Football Victory": { "Rookie Football Victory": {
"description": "Выиграйте матч", "description": "Выиграйте матч",
@ -258,21 +258,21 @@
"descriptionComplete": "Победа без смертей", "descriptionComplete": "Победа без смертей",
"descriptionFull": "Выиграйте уровень ${LEVEL} не умирая", "descriptionFull": "Выиграйте уровень ${LEVEL} не умирая",
"descriptionFullComplete": "Победа на уровне ${LEVEL} без смертей", "descriptionFullComplete": "Победа на уровне ${LEVEL} без смертей",
"name": "Остаться в живых" "name": "Поживем еще малец"
}, },
"Super Mega Punch": { "Super Mega Punch": {
"description": "Нанесите 100% урона одним ударом", "description": "Нанесите 100% урона одним ударом",
"descriptionComplete": "Нанесено 100% урона одним ударом", "descriptionComplete": "Нанесено 100% урона одним ударом",
"descriptionFull": "Нанесите 100% урона одним ударом на уровне ${LEVEL}", "descriptionFull": "Нанесите 100% урона одним ударом на уровне ${LEVEL}",
"descriptionFullComplete": "100% урона нанесено одним ударом на уровне ${LEVEL}", "descriptionFullComplete": "100% урона нанесено одним ударом на уровне ${LEVEL}",
"name": "Супер-мега-удар" "name": "Супер-Мега-Удар"
}, },
"Super Punch": { "Super Punch": {
"description": "Нанесите 50% урона одним ударом", "description": "Нанесите 50% урона одним ударом",
"descriptionComplete": "Нанесено 50% урона одним ударом", "descriptionComplete": "Нанесено 50% урона одним ударом",
"descriptionFull": "Нанесите 50% урона одним ударом на уровне ${LEVEL}", "descriptionFull": "Нанесите 50% урона одним ударом на уровне ${LEVEL}",
"descriptionFullComplete": "Нанесено 50% урона одним ударом на уровне ${LEVEL}", "descriptionFullComplete": "Нанесено 50% урона одним ударом на уровне ${LEVEL}",
"name": "Супер-удар" "name": "Супер-Удар"
}, },
"TNT Terror": { "TNT Terror": {
"description": "Убейте 6 негодяев с помощью TNT", "description": "Убейте 6 негодяев с помощью TNT",
@ -287,17 +287,17 @@
"name": "Командный игрок" "name": "Командный игрок"
}, },
"The Great Wall": { "The Great Wall": {
"description": "Остановите всех злодеев", "description": "Остановите всех негодяев",
"descriptionComplete": "Остановлены все злодеи", "descriptionComplete": "Остановлены все негодяи",
"descriptionFull": "Остановите всех злодеев до одного на уровне ${LEVEL}", "descriptionFull": "Остановите всех негодяев до одного на уровне ${LEVEL}",
"descriptionFullComplete": "Остановлены все злодеи на уровне ${LEVEL}", "descriptionFullComplete": "Остановлены все негодяи на уровне ${LEVEL}",
"name": "Великая стена" "name": "Великая Стена"
}, },
"The Wall": { "The Wall": {
"description": "Остановите всех злодеев", "description": "Остановите всех негодяев",
"descriptionComplete": "Остановлены все злодеи", "descriptionComplete": "Остановлены все негодяи",
"descriptionFull": "Остановите всех злодеев до одного на уровне ${LEVEL}", "descriptionFull": "Остановите всех негодяев до одного на уровне ${LEVEL}",
"descriptionFullComplete": "Остановлены все злодеи на уровне ${LEVEL}", "descriptionFullComplete": "Остановлены все негодяи на уровне ${LEVEL}",
"name": "Стена" "name": "Стена"
}, },
"Uber Football Shutout": { "Uber Football Shutout": {
@ -334,15 +334,15 @@
"achievementsUnavailableForOldSeasonsText": "К сожалению, подробности достижений не доступны для старых сезонов.", "achievementsUnavailableForOldSeasonsText": "К сожалению, подробности достижений не доступны для старых сезонов.",
"activatedText": "${THING} активировано.", "activatedText": "${THING} активировано.",
"addGameWindow": { "addGameWindow": {
"getMoreGamesText": "Еще игр...", "getMoreGamesText": "Еще игры",
"titleText": "Добавить игру" "titleText": "Добавить игру"
}, },
"allowText": "Разрешить", "allowText": "Разрешить",
"alreadySignedInText": "На вашем аккаунте играют на другом устройстве;\nпожалуйста зайдите с другого аккаунта или закройте\nигру на другом устройстве и попытайтесь снова.", "alreadySignedInText": "С вашего аккаунта играют на другом устройстве;\nпожалуйста зайдите с другого аккаунта или закройте\nигру на другом устройстве и попытайтесь снова.",
"apiVersionErrorText": "Невозможно загрузить модуль ${NAME}; он предназначен для API версии ${VERSION_USED}; здесь требуется версия ${VERSION_REQUIRED}.", "apiVersionErrorText": "Невозможно загрузить модуль ${NAME}; он предназначен для API версии ${VERSION_USED}; здесь требуется версия ${VERSION_REQUIRED}.",
"audioSettingsWindow": { "audioSettingsWindow": {
"headRelativeVRAudioInfoText": "(Режим \"Авто\" активируется только при подключении наушников)", "headRelativeVRAudioInfoText": "(Режим \"Авто\" активируется только при подключении наушников)",
"headRelativeVRAudioText": "Позиционно-зависимое ВР-аудио", "headRelativeVRAudioText": "Позиционно-зависимое VR-аудио",
"musicVolumeText": "Громкость музыки", "musicVolumeText": "Громкость музыки",
"soundVolumeText": "Громкость звука", "soundVolumeText": "Громкость звука",
"soundtrackButtonText": "Саундтреки", "soundtrackButtonText": "Саундтреки",
@ -369,7 +369,7 @@
"cantConfigureDeviceText": "Извините, ${DEVICE} невозможно настроить.", "cantConfigureDeviceText": "Извините, ${DEVICE} невозможно настроить.",
"challengeEndedText": "Это состязание завершено.", "challengeEndedText": "Это состязание завершено.",
"chatMuteText": "Заглушить чат", "chatMuteText": "Заглушить чат",
"chatMutedText": "Чат отключен", "chatMutedText": "Чат заглушен",
"chatUnMuteText": "Включить чат", "chatUnMuteText": "Включить чат",
"choosingPlayerText": "<выбор игрока>", "choosingPlayerText": "<выбор игрока>",
"completeThisLevelToProceedText": "Чтобы продолжить, нужно\nпройти этот уровень!", "completeThisLevelToProceedText": "Чтобы продолжить, нужно\nпройти этот уровень!",
@ -377,13 +377,13 @@
"configControllersWindow": { "configControllersWindow": {
"configureControllersText": "Настройка геймпада", "configureControllersText": "Настройка геймпада",
"configureGamepadsText": "Настройка контроллеров", "configureGamepadsText": "Настройка контроллеров",
"configureKeyboard2Text": "Настройка клавиатуры P2", "configureKeyboard2Text": "Настройка клавиатуры игрока 2",
"configureKeyboardText": "Настройка клавиатуры", "configureKeyboardText": "Настройка клавиатуры",
"configureMobileText": "Использовать мобильные устройства в качестве геймпадов", "configureMobileText": "Использовать мобильные устройства в качестве геймпадов",
"configureTouchText": "Настройка сенсорного экрана", "configureTouchText": "Настройка сенсорного экрана",
"ps3Text": "Геймпады PS3™", "ps3Text": "Геймпады PS3™",
"titleText": "Геймпады", "titleText": "Геймпады",
"wiimotesText": "Пульт Wii™", "wiimotesText": "Пульт Wiimote™",
"xbox360Text": "Геймпады Xbox 360™" "xbox360Text": "Геймпады Xbox 360™"
}, },
"configGamepadSelectWindow": { "configGamepadSelectWindow": {
@ -418,15 +418,15 @@
"pressAnyButtonText": "Нажмите любую кнопку", "pressAnyButtonText": "Нажмите любую кнопку",
"pressLeftRightText": "Нажмите вправо или влево...", "pressLeftRightText": "Нажмите вправо или влево...",
"pressUpDownText": "Нажмите вверх или вниз...", "pressUpDownText": "Нажмите вверх или вниз...",
"runButton1Text": "Кнопка для бега 1", "runButton1Text": "Кнопка бега 1",
"runButton2Text": "Кнопка для бега 2", "runButton2Text": "Кнопка бега 2",
"runTrigger1Text": "Триггер для бега 1", "runTrigger1Text": "Триггер бега 1",
"runTrigger2Text": "Триггер для бега 2", "runTrigger2Text": "Триггер бега 2",
"runTriggerDescriptionText": "(аналоговые триггеры позволяют бегать с разной скоростью)", "runTriggerDescriptionText": "(аналоговые триггеры позволяют бегать с разной скоростью)",
"secondHalfText": "Используйте эту опцию для настройки второй\nполовины устройства \"два геймпада в одном\",\nдля использования в качестве одного геймпада.", "secondHalfText": "Используйте эту опцию для настройки второй\nполовины устройства \"два геймпада в одном\",\nдля использования в качестве одного геймпада.",
"secondaryEnableText": "Включить", "secondaryEnableText": "Включить",
"secondaryText": "Второй геймпад", "secondaryText": "Второй геймпад",
"startButtonActivatesDefaultDescriptionText": "(выключить, если ваша кнопка \"старт\" работает больше в качестве кнопки \"меню\")", "startButtonActivatesDefaultDescriptionText": "(выключить, если ваша кнопка \"старт\" работает в качестве кнопки \"меню\")",
"startButtonActivatesDefaultText": "Кнопка Старт активирует стандартный виджет", "startButtonActivatesDefaultText": "Кнопка Старт активирует стандартный виджет",
"titleText": "Настройка геймпада", "titleText": "Настройка геймпада",
"twoInOneSetupText": "Настройка геймпада 2-в-1", "twoInOneSetupText": "Настройка геймпада 2-в-1",
@ -512,6 +512,7 @@
"welcome2Text": "Вы также можете заработать билеты от многих из тех же видов деятельности.\nБилеты могут быть использованы , чтобы разблокировать новые персонажи , карты и\nмини -игры, чтобы войти турниры, и многое другое.", "welcome2Text": "Вы также можете заработать билеты от многих из тех же видов деятельности.\nБилеты могут быть использованы , чтобы разблокировать новые персонажи , карты и\nмини -игры, чтобы войти турниры, и многое другое.",
"yourPowerRankingText": "Ваш ранг:" "yourPowerRankingText": "Ваш ранг:"
}, },
"copyConfirmText": "Скопировано в буфер обмена",
"copyOfText": "Копия ${NAME}", "copyOfText": "Копия ${NAME}",
"copyText": "Копия", "copyText": "Копия",
"copyrightText": "© 2013 Eric Froemling", "copyrightText": "© 2013 Eric Froemling",
@ -541,13 +542,13 @@
"deathsText": "Смерти", "deathsText": "Смерти",
"debugText": "отладка", "debugText": "отладка",
"debugWindow": { "debugWindow": {
"reloadBenchmarkBestResultsText": "Внимание: для этого теста рекомендуется установить Настройки->Графика->Текстуры на 'Высок.'", "reloadBenchmarkBestResultsText": "Внимание: для этого теста рекомендуется установить Настройки->Графика->Текстуры на 'Высокий'",
"runCPUBenchmarkText": "Запустить тест производительности CPU", "runCPUBenchmarkText": "Запустить тест производительности CPU",
"runGPUBenchmarkText": "Запустить тест производительности GPU", "runGPUBenchmarkText": "Запустить тест производительности GPU",
"runMediaReloadBenchmarkText": "Запустить тест производительности загрузки медиа", "runMediaReloadBenchmarkText": "Запустить тест производительности загрузки медиа",
"runStressTestText": "Выполнить тест-нагрузку", "runStressTestText": "Выполнить тест-нагрузку",
"stressTestPlayerCountText": "Количество игроков", "stressTestPlayerCountText": "Количество игроков",
"stressTestPlaylistDescriptionText": "плей-лист нагрузочного испытания", "stressTestPlaylistDescriptionText": "Плей-лист нагрузочного испытания",
"stressTestPlaylistNameText": "Название плей-листа", "stressTestPlaylistNameText": "Название плей-листа",
"stressTestPlaylistTypeText": "Тип плей-листа", "stressTestPlaylistTypeText": "Тип плей-листа",
"stressTestRoundDurationText": "Продолжительность раунда", "stressTestRoundDurationText": "Продолжительность раунда",
@ -563,7 +564,7 @@
"defaultNewTeamGameListNameText": "Мои командные игры", "defaultNewTeamGameListNameText": "Мои командные игры",
"defaultTeamGameListNameText": "Стандартные командные игры", "defaultTeamGameListNameText": "Стандартные командные игры",
"deleteText": "Удалить", "deleteText": "Удалить",
"demoText": "демонстрация", "demoText": "Демонстрация",
"denyText": "Отклонить", "denyText": "Отклонить",
"desktopResText": "Разреш. экрана", "desktopResText": "Разреш. экрана",
"difficultyEasyText": "Легкий", "difficultyEasyText": "Легкий",
@ -652,7 +653,7 @@
"epicDescriptionFilterText": "${DESCRIPTION} в эпическом замедленном действии.", "epicDescriptionFilterText": "${DESCRIPTION} в эпическом замедленном действии.",
"epicNameFilterText": "${NAME} в эпическом режиме", "epicNameFilterText": "${NAME} в эпическом режиме",
"errorAccessDeniedText": "доступ запрещен", "errorAccessDeniedText": "доступ запрещен",
"errorDeviceTimeIncorrectText": "Время на устройстве отстает на ${HOURS} часов.\nЭто вызывает проблемы.\nПожалуйста, проверьте настройки времени и часового пояса.", "errorDeviceTimeIncorrectText": "Время на устройстве отстает на ${HOURS} часов.\nЭто может вызывать проблемы.\nПожалуйста, проверьте настройки времени и часового пояса.",
"errorOutOfDiskSpaceText": "нет места на диске", "errorOutOfDiskSpaceText": "нет места на диске",
"errorSecureConnectionFailText": "Ошибка установки безопасного облачного соединения; сетевые функции могут дать сбой.", "errorSecureConnectionFailText": "Ошибка установки безопасного облачного соединения; сетевые функции могут дать сбой.",
"errorText": "Ошибка", "errorText": "Ошибка",
@ -751,7 +752,7 @@
"internetText": "Интернет", "internetText": "Интернет",
"inviteAFriendText": "Друзья еще не играют? Пригласи их\nпопробовать и они получат ${COUNT} билетов.", "inviteAFriendText": "Друзья еще не играют? Пригласи их\nпопробовать и они получат ${COUNT} билетов.",
"inviteFriendsText": "Пригласить друзей", "inviteFriendsText": "Пригласить друзей",
"joinPublicPartyDescriptionText": "Присоединитесь к публичной вечеринке", "joinPublicPartyDescriptionText": "Присоединитесь к публичному лобби",
"localNetworkDescriptionText": "Присоединяйтесь к ближайшему лобби (локальная сеть, Bluetooth, и т.д.)", "localNetworkDescriptionText": "Присоединяйтесь к ближайшему лобби (локальная сеть, Bluetooth, и т.д.)",
"localNetworkText": "Локальная сеть", "localNetworkText": "Локальная сеть",
"makePartyPrivateText": "Сделать мое лобби приватным", "makePartyPrivateText": "Сделать мое лобби приватным",
@ -783,7 +784,7 @@
"partyStatusJoinableText": "Ваша команда доступна через интернет", "partyStatusJoinableText": "Ваша команда доступна через интернет",
"partyStatusNoConnectionText": "Невозможно подключиться к серверу", "partyStatusNoConnectionText": "Невозможно подключиться к серверу",
"partyStatusNotJoinableText": "Ваше лобби недоступно через интернет", "partyStatusNotJoinableText": "Ваше лобби недоступно через интернет",
"partyStatusNotPublicText": "Ваше лобби не для всех", "partyStatusNotPublicText": "Ваше лобби не публично",
"pingText": "пинг", "pingText": "пинг",
"portText": "Порт", "portText": "Порт",
"privatePartyCloudDescriptionText": "Частные лобби работают на выделенных облачных серверах; настройка маршрутизатора не требуется.", "privatePartyCloudDescriptionText": "Частные лобби работают на выделенных облачных серверах; настройка маршрутизатора не требуется.",
@ -875,7 +876,7 @@
"controllersText": "Контроллеры", "controllersText": "Контроллеры",
"controlsSubtitleText": "У вашего дружелюбного персонажа из ${APP_NAME} есть несколько простых действий:", "controlsSubtitleText": "У вашего дружелюбного персонажа из ${APP_NAME} есть несколько простых действий:",
"controlsText": "Управление", "controlsText": "Управление",
"devicesInfoText": "В ВР-версию ${APP_NAME} можно играть по сети с обычной версией,\nтак что вытаскивайте свои дополнительные телефоны, планшеты\nи компьютеры, и играйте на них. Можно даже подключить\nобычную версию игры к ВР-версии, чтобы позволить\nостальным наблюдать за действием.", "devicesInfoText": "В VR-версию ${APP_NAME} можно играть по сети с обычной версией,\nтак что вытаскивайте свои дополнительные телефоны, планшеты\nи компьютеры, и играйте на них. Можно даже подключить\nобычную версию игры к VR-версии, чтобы позволить\nостальным наблюдать за действием.",
"devicesText": "Устройства", "devicesText": "Устройства",
"friendsGoodText": "Бывают полезны. В ${APP_NAME} веселее играть с несколькими игроками;\nподдерживается до 8 игроков одновременно, что приводит нас к:", "friendsGoodText": "Бывают полезны. В ${APP_NAME} веселее играть с несколькими игроками;\nподдерживается до 8 игроков одновременно, что приводит нас к:",
"friendsText": "Друзья", "friendsText": "Друзья",
@ -888,15 +889,15 @@
"powerupCurseNameText": "Проклятие", "powerupCurseNameText": "Проклятие",
"powerupHealthDescriptionText": "Ни за что не догадаетесь.\nВозвращает полное здоровье.", "powerupHealthDescriptionText": "Ни за что не догадаетесь.\nВозвращает полное здоровье.",
"powerupHealthNameText": "Аптечка", "powerupHealthNameText": "Аптечка",
"powerupIceBombsDescriptionText": "Слабее, чем обычные бомбы\nно делает врагов заморожеными\nи особенно хрупкими.", "powerupIceBombsDescriptionText": "Слабее, чем обычные бомбы\nно оставляет врагов заморожеными\nи чрезвычайно хрупкими.",
"powerupIceBombsNameText": "Ледяные бомбы", "powerupIceBombsNameText": "Ледяные бомбы",
"powerupImpactBombsDescriptionText": "Чуть слабее обычных бомб,\nно взрываются при ударе.", "powerupImpactBombsDescriptionText": "Чуть слабее обычных бомб,\nно взрываются при ударе.",
"powerupImpactBombsNameText": "Ударные бомбы", "powerupImpactBombsNameText": "Моментальные бомбы",
"powerupLandMinesDescriptionText": "Выдаются по 3 штуки.\nПолезны для защиты базы или\nусмирения быстроногих врагов.", "powerupLandMinesDescriptionText": "Выдаются по 3 штуки.\nПолезны для защиты базы или\nусмирения быстроногих врагов.",
"powerupLandMinesNameText": "Мины", "powerupLandMinesNameText": "Мины",
"powerupPunchDescriptionText": "Делают ваши удары быстрее,\nлучше, сильнее.", "powerupPunchDescriptionText": "Делают ваши удары быстрее,\nлучше, сильнее.",
"powerupPunchNameText": "Боксерские перчатки", "powerupPunchNameText": "Боксерские перчатки",
"powerupShieldDescriptionText": "Немного поглощает урон,\nчтобы вам не навредили.", "powerupShieldDescriptionText": "Немного поглощает урон,\nвместо вас.",
"powerupShieldNameText": "Энергетический щит", "powerupShieldNameText": "Энергетический щит",
"powerupStickyBombsDescriptionText": "Липнут ко всему, чего касаются.\nИ начинается веселье.", "powerupStickyBombsDescriptionText": "Липнут ко всему, чего касаются.\nИ начинается веселье.",
"powerupStickyBombsNameText": "Бомбы-липучки", "powerupStickyBombsNameText": "Бомбы-липучки",
@ -921,7 +922,7 @@
"internal": { "internal": {
"arrowsToExitListText": "чтобы выйти из списка нажмите ${LEFT} или ${RIGHT}", "arrowsToExitListText": "чтобы выйти из списка нажмите ${LEFT} или ${RIGHT}",
"buttonText": "кнопка", "buttonText": "кнопка",
"cantKickHostError": "Невозможно выгнать создателя.", "cantKickHostError": "Невозможно кикнуть создателя.",
"chatBlockedText": "${NAME} заблокирован на ${TIME} секунд.", "chatBlockedText": "${NAME} заблокирован на ${TIME} секунд.",
"connectedToGameText": "Вошел в игру '${NAME}'", "connectedToGameText": "Вошел в игру '${NAME}'",
"connectedToPartyText": "Вошел в лобби ${NAME}!", "connectedToPartyText": "Вошел в лобби ${NAME}!",
@ -976,7 +977,7 @@
"unableToResolveHostText": "Ошибка: невозможно достичь хоста.", "unableToResolveHostText": "Ошибка: невозможно достичь хоста.",
"unavailableNoConnectionText": "Сейчас это недоступно (нет интернет соединения?)", "unavailableNoConnectionText": "Сейчас это недоступно (нет интернет соединения?)",
"vrOrientationResetCardboardText": "Используйте это, чтобы сбросить ориентации VR.\nЧтобы играть в игру, вам понадобится внешний контроллер.", "vrOrientationResetCardboardText": "Используйте это, чтобы сбросить ориентации VR.\nЧтобы играть в игру, вам понадобится внешний контроллер.",
"vrOrientationResetText": "Сброс ориентации ВР.", "vrOrientationResetText": "Сброс ориентации VR.",
"willTimeOutText": "(время выйдет при бездействии)" "willTimeOutText": "(время выйдет при бездействии)"
}, },
"jumpBoldText": "ПРЫЖОК", "jumpBoldText": "ПРЫЖОК",
@ -986,11 +987,11 @@
"keyboardChangeInstructionsText": "Нажмите на пробел два раза, чтобы сменить раскладку.", "keyboardChangeInstructionsText": "Нажмите на пробел два раза, чтобы сменить раскладку.",
"keyboardNoOthersAvailableText": "Нету других раскладок.", "keyboardNoOthersAvailableText": "Нету других раскладок.",
"keyboardSwitchText": "Раскладка изменена на \"${NAME}\".", "keyboardSwitchText": "Раскладка изменена на \"${NAME}\".",
"kickOccurredText": "${NAME} изгнали.", "kickOccurredText": "${NAME} исключили.",
"kickQuestionText": згнать ${NAME}?", "kickQuestionText": сключить ${NAME}?",
"kickText": згнать", "kickText": сключить",
"kickVoteCantKickAdminsText": "Администраторов нельзя выгнать.", "kickVoteCantKickAdminsText": "Администраторов нельзя исключить.",
"kickVoteCantKickSelfText": "Вы не можете выгонять самого себя.", "kickVoteCantKickSelfText": "Вы не можете исключить самого себя (но можете выйти).",
"kickVoteFailedNotEnoughVotersText": "Недостаточно игроков для голосования.", "kickVoteFailedNotEnoughVotersText": "Недостаточно игроков для голосования.",
"kickVoteFailedText": "Голосование на вылет не удалось.", "kickVoteFailedText": "Голосование на вылет не удалось.",
"kickVoteStartedText": "Начато голосование за вылет ${NAME}.", "kickVoteStartedText": "Начато голосование за вылет ${NAME}.",
@ -1046,7 +1047,7 @@
"macControllerSubsystemTitleText": "Поддержка контроллера", "macControllerSubsystemTitleText": "Поддержка контроллера",
"mainMenu": { "mainMenu": {
"creditsText": "Благодарности", "creditsText": "Благодарности",
"demoMenuText": "Меню примеров", "demoMenuText": "Меню демо",
"endGameText": "Закончить игру", "endGameText": "Закончить игру",
"exitGameText": "Выйти из игры", "exitGameText": "Выйти из игры",
"exitToMenuText": "Выйти в меню?", "exitToMenuText": "Выйти в меню?",
@ -1060,7 +1061,7 @@
"resumeText": "Продолжить", "resumeText": "Продолжить",
"settingsText": "Настройки" "settingsText": "Настройки"
}, },
"makeItSoText": "Поехали!", "makeItSoText": "Да будет так",
"mapSelectGetMoreMapsText": "Ещё карт...", "mapSelectGetMoreMapsText": "Ещё карт...",
"mapSelectText": "Выбрать...", "mapSelectText": "Выбрать...",
"mapSelectTitleText": "Карты игры ${GAME}", "mapSelectTitleText": "Карты игры ${GAME}",
@ -1083,7 +1084,7 @@
"nameKilledText": "${NAME} убил ${VICTIM}.", "nameKilledText": "${NAME} убил ${VICTIM}.",
"nameNotEmptyText": "Имя не может быть пустым!", "nameNotEmptyText": "Имя не может быть пустым!",
"nameScoresText": "${NAME} ведет!", "nameScoresText": "${NAME} ведет!",
"nameSuicideKidFriendlyText": "${NAME} убился.", "nameSuicideKidFriendlyText": "${NAME} случайно убился.",
"nameSuicideText": "${NAME} совершил суицид.", "nameSuicideText": "${NAME} совершил суицид.",
"nameText": "Имя", "nameText": "Имя",
"nativeText": "Разрешение устройства", "nativeText": "Разрешение устройства",
@ -1168,7 +1169,7 @@
"playlistNotFoundText": "плей-лист не найден", "playlistNotFoundText": "плей-лист не найден",
"playlistText": "Плей-лист", "playlistText": "Плей-лист",
"playlistsText": "Плей-листы", "playlistsText": "Плей-листы",
"pleaseRateText": "Если вам нравится игра ${APP_NAME}, пожалуйста, подумайте о том,\nчтобы оценить ее или написать рецензию. Это обеспечивает полезную\nобратную связь и помогает поддержать дальнейшую разработку.\n\nСпасибо!\n- Эрик", "pleaseRateText": "Если вам нравится ${APP_NAME}, пожалуйста, подумайте о том,\nчтобы оценить ее или написать рецензию. Это обеспечивает полезную\nобратную связь и помогает поддержать дальнейшую разработку.\n\nСпасибо!\n- Эрик",
"pleaseWaitText": "Пожалуйста, подождите...", "pleaseWaitText": "Пожалуйста, подождите...",
"pluginClassLoadErrorText": "Ошибка при попытке загрузить класс плагина '${PLUGIN}': ${ERROR}", "pluginClassLoadErrorText": "Ошибка при попытке загрузить класс плагина '${PLUGIN}': ${ERROR}",
"pluginInitErrorText": "Ошибка при инициализации плагина '${PLUGIN}': ${ERROR}", "pluginInitErrorText": "Ошибка при инициализации плагина '${PLUGIN}': ${ERROR}",
@ -1182,7 +1183,7 @@
"pressAnyKeyButtonPlayAgainText": "Нажмите любую клавишу/кнопку чтобы играть снова...", "pressAnyKeyButtonPlayAgainText": "Нажмите любую клавишу/кнопку чтобы играть снова...",
"pressAnyKeyButtonText": "Нажмите любую клавишу/кнопку чтобы продолжить...", "pressAnyKeyButtonText": "Нажмите любую клавишу/кнопку чтобы продолжить...",
"pressAnyKeyText": "Нажмите любую клавишу...", "pressAnyKeyText": "Нажмите любую клавишу...",
"pressJumpToFlyText": "** Чтобы лететь, продолжайте нажимать прыжок **", "pressJumpToFlyText": "** Чтобы лететь, быстро нажимайте прыжок **",
"pressPunchToJoinText": "нажмите УДАР чтобы присоединиться...", "pressPunchToJoinText": "нажмите УДАР чтобы присоединиться...",
"pressToOverrideCharacterText": "нажмите ${BUTTONS} чтобы переопределить своего персонажа", "pressToOverrideCharacterText": "нажмите ${BUTTONS} чтобы переопределить своего персонажа",
"pressToSelectProfileText": "Нажмите ${BUTTONS} чтобы выбрать игрока", "pressToSelectProfileText": "Нажмите ${BUTTONS} чтобы выбрать игрока",
@ -1195,9 +1196,9 @@
}, },
"promoSubmitErrorText": "Ошибка отправки кода, проверьте своё интернете соединение", "promoSubmitErrorText": "Ошибка отправки кода, проверьте своё интернете соединение",
"ps3ControllersWindow": { "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остаться включенным и разрядит батарейки.", "ouyaInstructionsText": "Чтобы использовать геймпад PS3 с OUYA, просто подключите его один раз\nс помощью кабеля USB для синхронизации. Это может отключить другие\nгеймпады, тогда нужно перезагрузить OUYA и отсоединить кабель USB.\n\nПосле этого можно использовать кнопку 'PS' геймпада для беспроводного\nподключения. После игры нажмите и удерживайте кнопку 'PS' в течение\n10 секунд чтобы выключить геймпад, в противном случае он может\nостаться включенным и разрядит батарейки.",
"pairingTutorialText": "видео-тьюториал по синхронизации", "pairingTutorialText": "видео по связыванию",
"titleText": "Использование геймпада PS3 с ${APP_NAME}:" "titleText": "Использование геймпада PS3 с ${APP_NAME}:"
}, },
"publicBetaText": "ОТКРЫТАЯ БЕТА-ВЕРСИЯ", "publicBetaText": "ОТКРЫТАЯ БЕТА-ВЕРСИЯ",
@ -1218,8 +1219,8 @@
"remainingInTrialText": "осталось в пробной версии", "remainingInTrialText": "осталось в пробной версии",
"remoteAppInfoShortText": "Играть в ${APP_NAME} с семьей или друзьями гораздо веселее.\nПодключите один или несколько джойстиков или установите\n${REMOTE_APP_NAME} на свои устройства, чтобы использовать\nих в качестве джойстиков.", "remoteAppInfoShortText": "Играть в ${APP_NAME} с семьей или друзьями гораздо веселее.\nПодключите один или несколько джойстиков или установите\n${REMOTE_APP_NAME} на свои устройства, чтобы использовать\nих в качестве джойстиков.",
"remote_app": { "remote_app": {
"app_name": "ДУ BombSquad", "app_name": "Пульт BombSquad",
"app_name_short": "ДУBS", "app_name_short": "Пульт BS",
"button_position": "Положение кнопки", "button_position": "Положение кнопки",
"button_size": "Размер кнопки", "button_size": "Размер кнопки",
"cant_resolve_host": "Сервер не найден.", "cant_resolve_host": "Сервер не найден.",
@ -1434,6 +1435,7 @@
"tournamentStandingsText": "Позиции в турнире", "tournamentStandingsText": "Позиции в турнире",
"tournamentText": "Турнир", "tournamentText": "Турнир",
"tournamentTimeExpiredText": "Время турнира истекло", "tournamentTimeExpiredText": "Время турнира истекло",
"tournamentsDisabledWorkspaceText": "Турниры заблокированы пока рабочие пространства включены.\nДля включения турниров, отключите рабочие места и перезапустите игру.",
"tournamentsText": "Турниры", "tournamentsText": "Турниры",
"translations": { "translations": {
"characterNames": { "characterNames": {
@ -1485,22 +1487,22 @@
"${GAME} Training": "${GAME}: тренировка", "${GAME} Training": "${GAME}: тренировка",
"Infinite ${GAME}": "Бесконечный уровень ${GAME}", "Infinite ${GAME}": "Бесконечный уровень ${GAME}",
"Infinite Onslaught": "Бесконечная атака", "Infinite Onslaught": "Бесконечная атака",
"Infinite Runaround": "Бесконечный манёвр", "Infinite Runaround": "Бесконечная беготня",
"Onslaught": "Бесконечная атака", "Onslaught": "Бесконечная атака",
"Onslaught Training": "Атака: тренировка", "Onslaught Training": "Атака: тренировка",
"Pro ${GAME}": "${GAME} профи", "Pro ${GAME}": "${GAME} профи",
"Pro Football": "Регби профи", "Pro Football": "Регби профи",
"Pro Onslaught": "Атака профи", "Pro Onslaught": "Атака профи",
"Pro Runaround": "Манёвр профи", "Pro Runaround": "Беготня профи",
"Rookie ${GAME}": "${GAME} для новичков", "Rookie ${GAME}": "${GAME} для новичков",
"Rookie Football": "Регби для новичков", "Rookie Football": "Регби для новичков",
"Rookie Onslaught": "Атака для новичков", "Rookie Onslaught": "Атака для новичков",
"Runaround": "Бесконечный манёвр", "Runaround": "Бесконечный манёвр",
"The Last Stand": "Последний рубеж", "The Last Stand": "Последний рубеж",
"Uber ${GAME}": "Убер ${GAME}", "Uber ${GAME}": "Ӱбер ${GAME}",
"Uber Football": "Убер регби", "Uber Football": "Ӱбер регби",
"Uber Onslaught": "Убер атака", "Uber Onslaught": "Ӱбер атака",
"Uber Runaround": "Убер манёвр" "Uber Runaround": "Ӱбер беготня"
}, },
"gameDescriptions": { "gameDescriptions": {
"Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Чтобы победить, стань избранным на некоторое время.\nЧтобы стать избранным, убей избранного.", "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": "Избранный", "Chosen One": "Избранный",
"Conquest": "Завоевание", "Conquest": "Завоевание",
"Death Match": "Смертельный бой", "Death Match": "Смертельный бой",
"Easter Egg Hunt": "Охота на пасхальные яйца", "Easter Egg Hunt": "Сбор пасхальных яиц",
"Elimination": "Ликвидация", "Elimination": "Ликвидация",
"Football": "Регби", "Football": "Регби",
"Hockey": "Хоккей", "Hockey": "Хоккей",
@ -1580,14 +1582,14 @@
}, },
"inputDeviceNames": { "inputDeviceNames": {
"Keyboard": "Клавиатура", "Keyboard": "Клавиатура",
"Keyboard P2": "Клавиатура P2" "Keyboard P2": "Клавиатура игрока 2"
}, },
"languages": { "languages": {
"Arabic": "Арабский", "Arabic": "Арабский",
"Belarussian": "Белорусский", "Belarussian": "Белорусский",
"Chinese": "Китайский упрощенный", "Chinese": "Китайский упрощенный",
"ChineseTraditional": "Китайский традиционный", "ChineseTraditional": "Китайский традиционный",
"Croatian": "Харватский", "Croatian": "Хорватский",
"Czech": "Чешский", "Czech": "Чешский",
"Danish": "Датский", "Danish": "Датский",
"Dutch": "Голландский", "Dutch": "Голландский",
@ -1597,8 +1599,8 @@
"Finnish": "Финский", "Finnish": "Финский",
"French": "Французский", "French": "Французский",
"German": "Немецкий", "German": "Немецкий",
"Gibberish": "Абракадабра", "Gibberish": "Чепухейский",
"Greek": "греческий", "Greek": "Греческий",
"Hindi": "Хинди", "Hindi": "Хинди",
"Hungarian": "Венгерский", "Hungarian": "Венгерский",
"Indonesian": "Индонезийский", "Indonesian": "Индонезийский",
@ -1663,7 +1665,7 @@
}, },
"serverResponses": { "serverResponses": {
"A code has already been used on this account.": "Код уже был активирован на этом аккаунте.", "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 linking successful!": "Аккаунт успешно привязан!",
"Account unlinking successful!": "Аккаунт успешно отвязан!", "Account unlinking successful!": "Аккаунт успешно отвязан!",
"Accounts are already linked.": "Аккаунты уже привязаны.", "Accounts are already linked.": "Аккаунты уже привязаны.",
@ -1777,9 +1779,9 @@
"Warning to ${NAME}: turbo / button-spamming knocks you out.": "Предупреждение для ${NAME}: за турбо / быстрое повторное нажатие кнопки можно вылететь." "Warning to ${NAME}: turbo / button-spamming knocks you out.": "Предупреждение для ${NAME}: за турбо / быстрое повторное нажатие кнопки можно вылететь."
}, },
"teamNames": { "teamNames": {
"Bad Guys": "Плохие парни", "Bad Guys": "Негодяи",
"Blue": "Синие", "Blue": "Синие",
"Good Guys": "Хорошие парни", "Good Guys": "Добряки",
"Red": "Красные" "Red": "Красные"
}, },
"tips": { "tips": {
@ -1793,14 +1795,14 @@
"Don't spin for too long; you'll become dizzy and fall.": "Не крутись долго; у тебя закружится голова и ты упадёшь.", "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 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зато труднее поворачивать, так что не забывайте про обрывы.", "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 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 '${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 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 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 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 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 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 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или визуальные эффекты в настройках графики в игре.", "If your framerate is choppy, try turning down resolution\nor visuals in the game's graphics settings.": "Если картинка прерывистая, попробуйте уменьшить разрешение\nили визуальные эффекты в настройках графики в игре.",
@ -1860,12 +1862,12 @@
"phrase18Text": "В движении бросок получается дальше.", "phrase18Text": "В движении бросок получается дальше.",
"phrase19Text": "В прыжке бросок выше.", "phrase19Text": "В прыжке бросок выше.",
"phrase20Text": "\"Подкрученные\" бомбы летят еще дальше.", "phrase20Text": "\"Подкрученные\" бомбы летят еще дальше.",
"phrase21Text": "\"Выжидать\" бомбы довольно сложно.", "phrase21Text": "\"Подогревать\" бомбы довольно сложно.",
"phrase22Text": "Черт.", "phrase22Text": "Блин нафиг!",
"phrase23Text": "Попробуйте \"подогреть\" фитиль секунду или две.", "phrase23Text": "Попробуйте \"подогреть\" фитиль секунду или две.",
"phrase24Text": "Ура! Хорошо подогрето.", "phrase24Text": "Ура! Хорошо подогрето.",
"phrase25Text": "Ну на этом, пожалуй, всё.", "phrase25Text": "Ну на этом, пожалуй, всё.",
"phrase26Text": "Вперед, на мины!", "phrase26Text": "Вперед, к победе!",
"phrase27Text": "Не забывай эти советы, и ТОЧНО вернешься живым!", "phrase27Text": "Не забывай эти советы, и ТОЧНО вернешься живым!",
"phrase28Text": "...может быть...", "phrase28Text": "...может быть...",
"phrase29Text": "Удачи!", "phrase29Text": "Удачи!",
@ -1873,7 +1875,7 @@
"randomName2Text": "Петя", "randomName2Text": "Петя",
"randomName3Text": "Иннокентий", "randomName3Text": "Иннокентий",
"randomName4Text": "Шурик", "randomName4Text": "Шурик",
"randomName5Text": "Пушок", "randomName5Text": "Виталий",
"skipConfirmText": "Пропустить тьюториал? Коснитесь или нажмите кнопку для подтверждения.", "skipConfirmText": "Пропустить тьюториал? Коснитесь или нажмите кнопку для подтверждения.",
"skipVoteCountText": "${COUNT}/${TOTAL} голосов за пропуск", "skipVoteCountText": "${COUNT}/${TOTAL} голосов за пропуск",
"skippingText": "пропуск обучения...", "skippingText": "пропуск обучения...",

View file

@ -42,7 +42,7 @@
"ticketsText": "Boletos: ${COUNT}", "ticketsText": "Boletos: ${COUNT}",
"titleText": "Cuenta", "titleText": "Cuenta",
"unlinkAccountsInstructionsText": "Selecciona una cuenta para dejar de enlazar con ella", "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", "v2LinkInstructionsText": "Usa este encale para crearte una cuenta o para iniciar sesión",
"viaAccount": "(por cuenta ${NAME})", "viaAccount": "(por cuenta ${NAME})",
"youAreLoggedInAsText": "Estás conectado como:", "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.", "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:" "yourPowerRankingText": "Tu Clasificación de Poder:"
}, },
"copyConfirmText": "Copiado al portapapeles.",
"copyOfText": "${NAME} Copiar", "copyOfText": "${NAME} Copiar",
"copyText": "Copiar",
"createAPlayerProfileText": "¿Crear un perfil?", "createAPlayerProfileText": "¿Crear un perfil?",
"createEditPlayerText": "<Crear/Editar Jugador>", "createEditPlayerText": "<Crear/Editar Jugador>",
"createText": "Crear", "createText": "Crear",
@ -652,7 +654,7 @@
"epicDescriptionFilterText": "${DESCRIPTION} En cámara lenta épica.", "epicDescriptionFilterText": "${DESCRIPTION} En cámara lenta épica.",
"epicNameFilterText": "${NAME} - Modo épico", "epicNameFilterText": "${NAME} - Modo épico",
"errorAccessDeniedText": "acceso negado", "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", "errorOutOfDiskSpaceText": "insuficiente espacio en disco",
"errorSecureConnectionFailText": "No se puede establecer una conexión segura en la nube; La red podría estar fallando", "errorSecureConnectionFailText": "No se puede establecer una conexión segura en la nube; La red podría estar fallando",
"errorText": "Error", "errorText": "Error",
@ -889,11 +891,11 @@
"pickUpInfoText": "- Levanta -\nAlza banderas, enemigos, o cualquier\notra cosa no atornillada al suelo.\nPulsa de nuevo para lanzar.", "pickUpInfoText": "- Levanta -\nAlza banderas, enemigos, o cualquier\notra cosa no atornillada al suelo.\nPulsa de nuevo para lanzar.",
"pickUpInfoTextScale": 0.6, "pickUpInfoTextScale": 0.6,
"powerupBombDescriptionText": "Puedes tirar tres bombas de\nun solo tiro en vez de una sola.", "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?", "powerupCurseDescriptionText": "Probablemente querrás evitar estos.\n...¿o quizás no?",
"powerupCurseNameText": "Maldición", "powerupCurseNameText": "Maldición",
"powerupHealthDescriptionText": "Restaura toda la salud.\nNunca habrías imaginado.", "powerupHealthDescriptionText": "Restaura toda la salud.\nNunca lo hubieras adivinado.",
"powerupHealthNameText": "Caja de salud", "powerupHealthNameText": "Medicina",
"powerupIceBombsDescriptionText": "Más débil que las bombas habituales\npero dejan a tus enemigos congelados\ny particularmente frágiles.", "powerupIceBombsDescriptionText": "Más débil que las bombas habituales\npero dejan a tus enemigos congelados\ny particularmente frágiles.",
"powerupIceBombsNameText": "Bombas de hielo", "powerupIceBombsNameText": "Bombas de hielo",
"powerupImpactBombsDescriptionText": "Levemente más débiles que las bombas\nnormales, pero explotan al impacto.", "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.", "powerupPunchDescriptionText": "Hace que tus golpes sean más duros,\nmás rápidos, mejores, y más fuertes.",
"powerupPunchNameText": "Guantes de Boxeo", "powerupPunchNameText": "Guantes de Boxeo",
"powerupShieldDescriptionText": "Absorbe un poco del impacto\npara que tu no tengas que hacerlo.", "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.", "powerupStickyBombsDescriptionText": "Se adhieren a cualquier cosa.\nEn serio, es demasiado gracioso.",
"powerupStickyBombsNameText": "Bombas Pegajosas", "powerupStickyBombsNameText": "Bombas Pegajosas",
"powerupsSubtitleText": "Por supuesto, ningún juego está completo sin poderes extra:", "powerupsSubtitleText": "Por supuesto, ningún juego está completo sin poderes extra:",
@ -1084,7 +1086,7 @@
"modeClassicText": "Modo Clásico", "modeClassicText": "Modo Clásico",
"modeDemoText": "Modo De Demostración", "modeDemoText": "Modo De Demostración",
"mostValuablePlayerText": "Jugador más Valorado", "mostValuablePlayerText": "Jugador más Valorado",
"mostViolatedPlayerText": "Jugador más Violado", "mostViolatedPlayerText": "Jugador más Agredido",
"mostViolentPlayerText": "Jugador más Violento", "mostViolentPlayerText": "Jugador más Violento",
"moveText": "Mover", "moveText": "Mover",
"multiKillText": "¡¡¡${COUNT}-COMBO!!!", "multiKillText": "¡¡¡${COUNT}-COMBO!!!",
@ -1450,6 +1452,7 @@
"tournamentStandingsText": "Puestos del Torneo", "tournamentStandingsText": "Puestos del Torneo",
"tournamentText": "Torneo", "tournamentText": "Torneo",
"tournamentTimeExpiredText": "Tiempo del Torneo Expirado", "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", "tournamentsText": "Torneos",
"translations": { "translations": {
"characterNames": { "characterNames": {
@ -1466,9 +1469,9 @@
"Jack Morgan": "Jack Morgan", "Jack Morgan": "Jack Morgan",
"Kronk": "Kronk", "Kronk": "Kronk",
"Lee": "Lee", "Lee": "Lee",
"Lucky": "Suertudo", "Lucky": "Lucky",
"Mel": "Mel", "Mel": "Mel",
"Middle-Man": "Middle-Man", "Middle-Man": "Intermediario",
"Minimus": "Minimus", "Minimus": "Minimus",
"Pascal": "Pascal", "Pascal": "Pascal",
"Pixel": "Pixel", "Pixel": "Pixel",
@ -1643,21 +1646,21 @@
"Silver": "Plata" "Silver": "Plata"
}, },
"mapsNames": { "mapsNames": {
"Big G": "La Gran G", "Big G": "Gran G",
"Bridgit": "Puentecito", "Bridgit": "Puentecito",
"Courtyard": "Patio Real", "Courtyard": "Patio Real",
"Crag Castle": "Castillo de Piedra", "Crag Castle": "Castillo del Risco",
"Doom Shroom": "Hongo de la Muerte", "Doom Shroom": "Hongo de la Muerte",
"Football Stadium": "Estadio de Fútbol", "Football Stadium": "Estadio de Fútbol",
"Happy Thoughts": "Pensamientos felices", "Happy Thoughts": "Sueños Felices",
"Hockey Stadium": "Estadio de Hockey", "Hockey Stadium": "Estadio de Hockey",
"Lake Frigid": "Lago Frígido", "Lake Frigid": "Lago Frígido",
"Monkey Face": "Cara de Mono", "Monkey Face": "Cara de Mono",
"Rampage": "Medio Tubo", "Rampage": "Rampa",
"Roundabout": "Rotonda", "Roundabout": "Rotonda",
"Step Right Up": "Paso al Frente", "Step Right Up": "Paso al Frente",
"The Pad": "La Plataforma", "The Pad": "La Plataforma",
"Tip Top": "La Montaña", "Tip Top": "Montaña",
"Tower D": "Torre D", "Tower D": "Torre D",
"Zigzag": "Zigzag" "Zigzag": "Zigzag"
}, },

View file

@ -497,6 +497,7 @@
"welcome2Text": "இதே போன்ற பல செயல்களில் இருந்து நீங்கள் டிக்கெட்டுகளைப் பெறலாம்.\nபுதிய எழுத்துக்கள், வரைபடங்கள் மற்றும் பலவற்றைத் திறக்க டிக்கெட்டுகளைப் பயன்படுத்தலாம்\nசிறு விளையாட்டுகள், போட்டிகளில் நுழைய, மற்றும் பல.", "welcome2Text": "இதே போன்ற பல செயல்களில் இருந்து நீங்கள் டிக்கெட்டுகளைப் பெறலாம்.\nபுதிய எழுத்துக்கள், வரைபடங்கள் மற்றும் பலவற்றைத் திறக்க டிக்கெட்டுகளைப் பயன்படுத்தலாம்\nசிறு விளையாட்டுகள், போட்டிகளில் நுழைய, மற்றும் பல.",
"yourPowerRankingText": "உங்கள் சக்தி தரவரிசை:" "yourPowerRankingText": "உங்கள் சக்தி தரவரிசை:"
}, },
"copyConfirmText": "கிளிப்போர்டுக்கு நகலெடுக்கப்பட்டது.",
"copyOfText": "${NAME} பிரதி", "copyOfText": "${NAME} பிரதி",
"copyText": "நகல்", "copyText": "நகல்",
"createEditPlayerText": "<பிளேயரை உருவாக்கவும்/திருத்தவும்>", "createEditPlayerText": "<பிளேயரை உருவாக்கவும்/திருத்தவும்>",
@ -624,7 +625,7 @@
"epicDescriptionFilterText": "${DESCRIPTION} காவிய மெதுவான இயக்கத்தில்.", "epicDescriptionFilterText": "${DESCRIPTION} காவிய மெதுவான இயக்கத்தில்.",
"epicNameFilterText": "காவியம் ${NAME}", "epicNameFilterText": "காவியம் ${NAME}",
"errorAccessDeniedText": "அணுகல் மறுக்கப்பட்டது", "errorAccessDeniedText": "அணுகல் மறுக்கப்பட்டது",
"errorDeviceTimeIncorrectText": "உங்கள் சாதனத்தின் நேரம் ${HOURS} மணிநேரம் உள்ளது.\nஇதனால் பிரச்னைகள் ஏற்பட வாய்ப்புள்ளது.\nஉங்கள் நேரம் மற்றும் நேர மண்டல அமைப்புகளைச் சரிபார்க்கவும்.", "errorDeviceTimeIncorrectText": "உங்கள் சாதனத்தின் நேரம் ${HOURS} மணிநேரம் தவறாக உள்ளது.\nஇதனால் பிரச்னைகள் ஏற்பட வாய்ப்புள்ளது.\nஉங்கள் நேரம் மற்றும் நேர மண்டல அமைப்புகளைச் சரிபார்க்கவும்.",
"errorOutOfDiskSpaceText": "வட்டு இடத்திற்கு வெளியே", "errorOutOfDiskSpaceText": "வட்டு இடத்திற்கு வெளியே",
"errorSecureConnectionFailText": "பாதுகாப்பான கிளவுட் இணைப்பை நிறுவ முடியவில்லை; பிணைய செயல்பாடு தோல்வியடையலாம்.", "errorSecureConnectionFailText": "பாதுகாப்பான கிளவுட் இணைப்பை நிறுவ முடியவில்லை; பிணைய செயல்பாடு தோல்வியடையலாம்.",
"errorText": "பிழை", "errorText": "பிழை",
@ -1368,6 +1369,7 @@
"tournamentStandingsText": "போட்டி நிலைகள்", "tournamentStandingsText": "போட்டி நிலைகள்",
"tournamentText": "போட்டி", "tournamentText": "போட்டி",
"tournamentTimeExpiredText": "போட்டி நேரம் காலாவதியானது", "tournamentTimeExpiredText": "போட்டி நேரம் காலாவதியானது",
"tournamentsDisabledWorkspaceText": "பணியிடங்கள் செயலில் இருக்கும்போது போட்டிகள் முடக்கப்படும்.\nபோட்டிகளை மீண்டும் இயக்க, உங்கள் பணியிடத்தை முடக்கி மீண்டும் தொடங்கவும்.",
"tournamentsText": "போட்டிகள்", "tournamentsText": "போட்டிகள்",
"translations": { "translations": {
"characterNames": { "characterNames": {
@ -1857,7 +1859,7 @@
"worldsBestScoresText": "உலகின் சிறந்த மதிப்பெண்கள்", "worldsBestScoresText": "உலகின் சிறந்த மதிப்பெண்கள்",
"worldsBestTimesText": "உலகின் சிறந்த நேரங்கள்", "worldsBestTimesText": "உலகின் சிறந்த நேரங்கள்",
"xbox360ControllersWindow": { "xbox360ControllersWindow": {
"getDriverText": "Driver ஐ பெ", "getDriverText": "Driver ஐ பெரு",
"macInstructions2Text": "கட்டுப்படுத்திகளை கம்பியில்லாமல் பயன்படுத்த, உங்களுக்கு ரிசீவரும் தேவை\nவிண்டோஸிற்கான எக்ஸ்பாக்ஸ் 360 வயர்லெஸ் கன்ட்ரோலருடன் வருகிறது.\nஒரு ரிசீவர் உங்களை 4 கட்டுப்படுத்திகளை இணைக்க அனுமதிக்கிறது.\n\nமுக்கியமானது: 3 வது தரப்பு பெறுநர்கள் இந்த டிரைவருடன் வேலை செய்ய மாட்டார்கள்;\nஉங்கள் ரிசீவர் அதில் 'மைக்ரோசாப்ட்' என்று கூறுவதை உறுதி செய்து கொள்ளுங்கள், 'எக்ஸ்பாக்ஸ் 360' அல்ல.\nமைக்ரோசாப்ட் இனி தனித்தனியாக விற்காது, எனவே நீங்கள் பெற வேண்டும்\nகட்டுப்பாட்டாளருடன் தொகுக்கப்பட்ட ஒன்று அல்லது வேறு ஈபேயைத் தேடுங்கள்.\n\nஇது உங்களுக்கு பயனுள்ளதாக இருந்தால், தயவுசெய்து ஒரு நன்கொடையைக் கருத்தில் கொள்ளவும்\nஅவரது தளத்தில் டிரைவர் டெவலப்பர்.", "macInstructions2Text": "கட்டுப்படுத்திகளை கம்பியில்லாமல் பயன்படுத்த, உங்களுக்கு ரிசீவரும் தேவை\nவிண்டோஸிற்கான எக்ஸ்பாக்ஸ் 360 வயர்லெஸ் கன்ட்ரோலருடன் வருகிறது.\nஒரு ரிசீவர் உங்களை 4 கட்டுப்படுத்திகளை இணைக்க அனுமதிக்கிறது.\n\nமுக்கியமானது: 3 வது தரப்பு பெறுநர்கள் இந்த டிரைவருடன் வேலை செய்ய மாட்டார்கள்;\nஉங்கள் ரிசீவர் அதில் 'மைக்ரோசாப்ட்' என்று கூறுவதை உறுதி செய்து கொள்ளுங்கள், 'எக்ஸ்பாக்ஸ் 360' அல்ல.\nமைக்ரோசாப்ட் இனி தனித்தனியாக விற்காது, எனவே நீங்கள் பெற வேண்டும்\nகட்டுப்பாட்டாளருடன் தொகுக்கப்பட்ட ஒன்று அல்லது வேறு ஈபேயைத் தேடுங்கள்.\n\nஇது உங்களுக்கு பயனுள்ளதாக இருந்தால், தயவுசெய்து ஒரு நன்கொடையைக் கருத்தில் கொள்ளவும்\nஅவரது தளத்தில் டிரைவர் டெவலப்பர்.",
"macInstructionsText": "எக்ஸ்பாக்ஸ் 360 கட்டுப்படுத்திகளைப் பயன்படுத்த, நீங்கள் நிறுவ வேண்டும்\nமேக் டிரைவர் கீழே உள்ள இணைப்பில் கிடைக்கிறது.\nஇது கம்பி மற்றும் வயர்லெஸ் கட்டுப்படுத்திகளுடன் வேலை செய்கிறது.", "macInstructionsText": "எக்ஸ்பாக்ஸ் 360 கட்டுப்படுத்திகளைப் பயன்படுத்த, நீங்கள் நிறுவ வேண்டும்\nமேக் டிரைவர் கீழே உள்ள இணைப்பில் கிடைக்கிறது.\nஇது கம்பி மற்றும் வயர்லெஸ் கட்டுப்படுத்திகளுடன் வேலை செய்கிறது.",
"ouyaInstructionsText": "BombSquad உடன் கம்பி எக்ஸ்பாக்ஸ் 360 கட்டுப்படுத்திகளைப் பயன்படுத்த, வெறுமனே\nஅவற்றை உங்கள் சாதனத்தின் USB போர்ட்டில் செருகவும். நீங்கள் ஒரு USB ஹப் பயன்படுத்தலாம்\nபல கட்டுப்படுத்திகளை இணைக்க.\n\nவயர்லெஸ் கன்ட்ரோலர்களைப் பயன்படுத்த உங்களுக்கு வயர்லெஸ் ரிசீவர் தேவை,\n\"விண்டோஸிற்கான எக்ஸ்பாக்ஸ் 360 வயர்லெஸ் கன்ட்ரோலர்\" இன் ஒரு பகுதியாக கிடைக்கிறது\nதொகுப்பு அல்லது தனித்தனியாக விற்கப்படுகிறது. ஒவ்வொரு ரிசீவரும் USB போர்ட்டில் செருகப்படுகிறது மற்றும்\n4 வயர்லெஸ் கட்டுப்படுத்திகளை இணைக்க உங்களை அனுமதிக்கிறது.", "ouyaInstructionsText": "BombSquad உடன் கம்பி எக்ஸ்பாக்ஸ் 360 கட்டுப்படுத்திகளைப் பயன்படுத்த, வெறுமனே\nஅவற்றை உங்கள் சாதனத்தின் USB போர்ட்டில் செருகவும். நீங்கள் ஒரு USB ஹப் பயன்படுத்தலாம்\nபல கட்டுப்படுத்திகளை இணைக்க.\n\nவயர்லெஸ் கன்ட்ரோலர்களைப் பயன்படுத்த உங்களுக்கு வயர்லெஸ் ரிசீவர் தேவை,\n\"விண்டோஸிற்கான எக்ஸ்பாக்ஸ் 360 வயர்லெஸ் கன்ட்ரோலர்\" இன் ஒரு பகுதியாக கிடைக்கிறது\nதொகுப்பு அல்லது தனித்தனியாக விற்கப்படுகிறது. ஒவ்வொரு ரிசீவரும் USB போர்ட்டில் செருகப்படுகிறது மற்றும்\n4 வயர்லெஸ் கட்டுப்படுத்திகளை இணைக்க உங்களை அனுமதிக்கிறது.",

View file

@ -325,6 +325,7 @@
"achievementsRemainingText": "ความสำเร็จที่ยังเหลืออยู่:", "achievementsRemainingText": "ความสำเร็จที่ยังเหลืออยู่:",
"achievementsText": "ความสำเร็จ", "achievementsText": "ความสำเร็จ",
"achievementsUnavailableForOldSeasonsText": "ขออภัย, ความสำเร็จนี้ไม่ได้มีอยู่ในฤดูกาลเก่า", "achievementsUnavailableForOldSeasonsText": "ขออภัย, ความสำเร็จนี้ไม่ได้มีอยู่ในฤดูกาลเก่า",
"activatedText": "เปิดใช้งาน ${THING} แล้ว",
"addGameWindow": { "addGameWindow": {
"getMoreGamesText": "รับเกมเพิ่มเติม", "getMoreGamesText": "รับเกมเพิ่มเติม",
"titleText": "เพิ่มเกม" "titleText": "เพิ่มเกม"
@ -622,7 +623,9 @@
"epicDescriptionFilterText": "${DESCRIPTION} ในการเคลื่อนไหวที่ช้ามากๆ", "epicDescriptionFilterText": "${DESCRIPTION} ในการเคลื่อนไหวที่ช้ามากๆ",
"epicNameFilterText": "${NAME} แบบช้ามหากาฬ", "epicNameFilterText": "${NAME} แบบช้ามหากาฬ",
"errorAccessDeniedText": "การเข้าถึงถูกปฏิเสธ", "errorAccessDeniedText": "การเข้าถึงถูกปฏิเสธ",
"errorDeviceTimeIncorrectText": "เวลาของอุปกรณ์ของคุณปิดลง ${HOURS} ชั่วโมง\nมีแนวโน้มที่จะทำให้เกิดปัญหา\nโปรดตรวจสอบการตั้งค่าเวลาและเขตเวลาของคุณ",
"errorOutOfDiskSpaceText": "พื้นที่ว่างในเครื่องหมด", "errorOutOfDiskSpaceText": "พื้นที่ว่างในเครื่องหมด",
"errorSecureConnectionFailText": "ไม่สามารถสร้างการเชื่อมต่อระบบคลาวด์ที่ปลอดภัยได้ การทำงานของเครือข่ายอาจล้มเหลว",
"errorText": "ข้อผิดพลาด", "errorText": "ข้อผิดพลาด",
"errorUnknownText": "ข้อผิดพลาดที่ไม่รู้จัก", "errorUnknownText": "ข้อผิดพลาดที่ไม่รู้จัก",
"exitGameText": "จะออกจาก ${APP_NAME} หรือไม่?", "exitGameText": "จะออกจาก ${APP_NAME} หรือไม่?",
@ -785,7 +788,7 @@
"ticketPack4Text": "แพ็กตั๋วจัมโบ้", "ticketPack4Text": "แพ็กตั๋วจัมโบ้",
"ticketPack5Text": "แพ็กตั๋วแมมมอธ", "ticketPack5Text": "แพ็กตั๋วแมมมอธ",
"ticketPack6Text": "แพ็กตั๋วสูงสุด", "ticketPack6Text": "แพ็กตั๋วสูงสุด",
"ticketsFromASponsorText": "รับตั๋ว ${COUNT} ใบ\nจากสปอนเซอร์", "ticketsFromASponsorText": "ดูโฆษณา\nเพื่อรับตั๋ว ${COUNT} ใบ",
"ticketsText": "ตั๋ว ${COUNT} ใบ", "ticketsText": "ตั๋ว ${COUNT} ใบ",
"titleText": "รับตั๋ว", "titleText": "รับตั๋ว",
"unavailableLinkAccountText": "ขออภัย ไม่สามารถซื้อได้บนแพลตฟอร์มนี้\nวิธีแก้ปัญหา คุณสามารถเชื่อมโยงบัญชีนี้กับบัญชีบน\nแพลตฟอร์มอื่นและทำการซื้อที่นั่น", "unavailableLinkAccountText": "ขออภัย ไม่สามารถซื้อได้บนแพลตฟอร์มนี้\nวิธีแก้ปัญหา คุณสามารถเชื่อมโยงบัญชีนี้กับบัญชีบน\nแพลตฟอร์มอื่นและทำการซื้อที่นั่น",
@ -796,6 +799,7 @@
"youHaveText": "คุณมีตั๋ว ${COUNT} ใบ" "youHaveText": "คุณมีตั๋ว ${COUNT} ใบ"
}, },
"googleMultiplayerDiscontinuedText": "ขออภัย บริการผู้เล่นหลายคนของ Google ไม่มีให้บริการอีกต่อไป\nฉันกำลังดำเนินการเปลี่ยนให้เร็วที่สุด\nในระหว่างนี้ โปรดลองวิธีการเชื่อมต่ออื่น\n-เอริค", "googleMultiplayerDiscontinuedText": "ขออภัย บริการผู้เล่นหลายคนของ Google ไม่มีให้บริการอีกต่อไป\nฉันกำลังดำเนินการเปลี่ยนให้เร็วที่สุด\nในระหว่างนี้ โปรดลองวิธีการเชื่อมต่ออื่น\n-เอริค",
"googlePlayPurchasesNotAvailableText": "ไม่สามารถซื้อด้วย Google Play ได้\nคุณอาจต้องอัปเดตแอปร้านค้าของคุณ",
"googlePlayText": "Google Play", "googlePlayText": "Google Play",
"graphicsSettingsWindow": { "graphicsSettingsWindow": {
"alwaysText": "ตลอด", "alwaysText": "ตลอด",
@ -1106,7 +1110,10 @@
"playlistsText": "เพลย์ลิส", "playlistsText": "เพลย์ลิส",
"pleaseRateText": "หากคุณชอบ ${APP_NAME} โปรดพิจารณาใช้ a\nและให้คะแนนหรือเขียนรีวิว นี้ให้\nข้อเสนอแนะที่เป็นประโยชน์และช่วยสนับสนุนการพัฒนาในอนาคต\n\nขอบใจ!\n-eric", "pleaseRateText": "หากคุณชอบ ${APP_NAME} โปรดพิจารณาใช้ a\nและให้คะแนนหรือเขียนรีวิว นี้ให้\nข้อเสนอแนะที่เป็นประโยชน์และช่วยสนับสนุนการพัฒนาในอนาคต\n\nขอบใจ!\n-eric",
"pleaseWaitText": "โปรดรอ...", "pleaseWaitText": "โปรดรอ...",
"pluginsDetectedText": "ตรวจพบปลั๊กอินใหม่ เปิด/กำหนดค่าได้ในการตั้งค่า", "pluginClassLoadErrorText": "เกิดข้อผิดพลาดในการโหลดคลาสปลั๊กอิน '${PLUGIN}': ${ERROR}",
"pluginInitErrorText": "เกิดข้อผิดพลาดในการเริ่มต้นปลั๊กอิน '${PLUGIN}': ${ERROR}",
"pluginsDetectedText": "ตรวจพบปลั๊กอินใหม่ รีสตาร์ทเกมเพื่อเปิดใช้งาน หรือกำหนดค่าได้ในการตั้งค่า",
"pluginsRemovedText": "ไม่พบปลั๊กอิน ${NUM} รายการอีกต่อไป",
"pluginsText": "ปลั๊กอิน", "pluginsText": "ปลั๊กอิน",
"practiceText": "ฝึกฝน", "practiceText": "ฝึกฝน",
"pressAnyButtonPlayAgainText": "กดปุ่มใดก็ได้เพื่อเล่นอีกครั้ง...", "pressAnyButtonPlayAgainText": "กดปุ่มใดก็ได้เพื่อเล่นอีกครั้ง...",
@ -1357,6 +1364,7 @@
"tournamentStandingsText": "อันดับการแข่งขัน", "tournamentStandingsText": "อันดับการแข่งขัน",
"tournamentText": "การแข่งขัน", "tournamentText": "การแข่งขัน",
"tournamentTimeExpiredText": "เวลาการแข่งขันหมดอายุ", "tournamentTimeExpiredText": "เวลาการแข่งขันหมดอายุ",
"tournamentsDisabledWorkspaceText": "การแข่งขันจะถูกปิดการใช้งานเมื่อมีการใช้งานพื้นที่ทำงาน \nหากต้องการเปิดใช้งานการแข่งขันอีกครั้ง ให้ปิดใช้งานพื้นที่ทำงานของคุณแล้วเริ่มใหม่",
"tournamentsText": "การแข่งขัน", "tournamentsText": "การแข่งขัน",
"translations": { "translations": {
"characterNames": { "characterNames": {
@ -1840,6 +1848,8 @@
"winsPlayerText": "${NAME} ชนะ!", "winsPlayerText": "${NAME} ชนะ!",
"winsTeamText": "${NAME} ชนะ!", "winsTeamText": "${NAME} ชนะ!",
"winsText": "${NAME} ชนะ!", "winsText": "${NAME} ชนะ!",
"workspaceSyncErrorText": "เกิดข้อผิดพลาดในการซิงค์ ${WORKSPACE} เปิดlogเพื่อดูรายละเอียด",
"workspaceSyncReuseText": "ไม่สามารถซิงค์ ${WORKSPACE} ได้ จึงนำเวอร์ชันที่ซิงค์ก่อนหน้านี้มาใช้",
"worldScoresUnavailableText": "ไม่มีคะแนนโลก", "worldScoresUnavailableText": "ไม่มีคะแนนโลก",
"worldsBestScoresText": "คะแนนที่ดีที่สุดในโลก", "worldsBestScoresText": "คะแนนที่ดีที่สุดในโลก",
"worldsBestTimesText": "เวลาที่ดีที่สุดในโลก", "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.", "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:" "yourPowerRankingText": "Oyuncu Sıralaman:"
}, },
"copyConfirmText": "Panoya kopyalandı.",
"copyOfText": "Kopya ${NAME}", "copyOfText": "Kopya ${NAME}",
"copyText": "Kopyala", "copyText": "Kopyala",
"createEditPlayerText": "<Oyuncu Oluştur/Düzenle>", "createEditPlayerText": "<Oyuncu Oluştur/Düzenle>",
@ -625,7 +626,7 @@
"epicDescriptionFilterText": "${DESCRIPTION} epik ağırçekim.", "epicDescriptionFilterText": "${DESCRIPTION} epik ağırçekim.",
"epicNameFilterText": "Epik ${NAME}", "epicNameFilterText": "Epik ${NAME}",
"errorAccessDeniedText": "erişim reddedildi", "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", "errorOutOfDiskSpaceText": "disk alanı doldu",
"errorSecureConnectionFailText": "Güvenli bulut bağlantısı kurulamadı; ağ işlevi başarısız olabilir.", "errorSecureConnectionFailText": "Güvenli bulut bağlantısı kurulamadı; ağ işlevi başarısız olabilir.",
"errorText": "Hata", "errorText": "Hata",
@ -1367,6 +1368,7 @@
"tournamentStandingsText": "Turnuva Kazananları", "tournamentStandingsText": "Turnuva Kazananları",
"tournamentText": "Turnuva", "tournamentText": "Turnuva",
"tournamentTimeExpiredText": "Turnuva Sona Erdi", "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", "tournamentsText": "Turnuvalar",
"translations": { "translations": {
"characterNames": { "characterNames": {

View file

@ -328,6 +328,7 @@
"achievementsRemainingText": "Досягнень залишилось:", "achievementsRemainingText": "Досягнень залишилось:",
"achievementsText": "Досягнення", "achievementsText": "Досягнення",
"achievementsUnavailableForOldSeasonsText": "На жаль, специфіка досягнення недоступна для старих сезонів.", "achievementsUnavailableForOldSeasonsText": "На жаль, специфіка досягнення недоступна для старих сезонів.",
"activatedText": "${THING} активовано",
"addGameWindow": { "addGameWindow": {
"getMoreGamesText": "Ще ігри...", "getMoreGamesText": "Ще ігри...",
"titleText": "Додати гру" "titleText": "Додати гру"
@ -625,7 +626,9 @@
"epicDescriptionFilterText": "${DESCRIPTION} в епічному сповільненій дії.", "epicDescriptionFilterText": "${DESCRIPTION} в епічному сповільненій дії.",
"epicNameFilterText": "${NAME} в епічному режимі", "epicNameFilterText": "${NAME} в епічному режимі",
"errorAccessDeniedText": "доступ заборонено", "errorAccessDeniedText": "доступ заборонено",
"errorDeviceTimeIncorrectText": "Час вашого пристрою зміщено на${HOURS} год.\nЦе може спричинити проблеми.\nБудь ласка, перевірте свій час і налаштування часового поясу.",
"errorOutOfDiskSpaceText": "немає місця на диску", "errorOutOfDiskSpaceText": "немає місця на диску",
"errorSecureConnectionFailText": "Неможливо встановити безпечне хмарне з’єднання; мережеві функції можуть не працювати.",
"errorText": "Помилка", "errorText": "Помилка",
"errorUnknownText": "невідома помилка", "errorUnknownText": "невідома помилка",
"exitGameText": "Вийти з ${APP_NAME}?", "exitGameText": "Вийти з ${APP_NAME}?",
@ -788,7 +791,7 @@
"ticketPack4Text": "Величезна пачка квитків", "ticketPack4Text": "Величезна пачка квитків",
"ticketPack5Text": "Слонова пачка квитків", "ticketPack5Text": "Слонова пачка квитків",
"ticketPack6Text": "Максимальна пачка квитків", "ticketPack6Text": "Максимальна пачка квитків",
"ticketsFromASponsorText": "Отримати ${COUNT} квитків\nвід спонсора", "ticketsFromASponsorText": "Перегляньте рекламу\nза ${COUNT} квитків",
"ticketsText": "Квитків: ${COUNT}", "ticketsText": "Квитків: ${COUNT}",
"titleText": "Отримати квитки", "titleText": "Отримати квитки",
"unavailableLinkAccountText": "Вибачте, але на цій платформі покупки недоступні.\nВ якості вирішення, ви можете прив'язати цей акаунт\nдо акаунту на іншій платформі, і здійснювати покупки там.", "unavailableLinkAccountText": "Вибачте, але на цій платформі покупки недоступні.\nВ якості вирішення, ви можете прив'язати цей акаунт\nдо акаунту на іншій платформі, і здійснювати покупки там.",
@ -799,6 +802,7 @@
"youHaveText": "У вас ${COUNT} квитків" "youHaveText": "У вас ${COUNT} квитків"
}, },
"googleMultiplayerDiscontinuedText": "Пробачте, але сервіс мультіплеєра від Google тепер не доступний.\nЯ працюю над зміною сервіса як можно скоріше.\nДо цього, будь ласка, подивіться інакші способи гри в мультіплеєр. \n-Ерік", "googleMultiplayerDiscontinuedText": "Пробачте, але сервіс мультіплеєра від Google тепер не доступний.\nЯ працюю над зміною сервіса як можно скоріше.\nДо цього, будь ласка, подивіться інакші способи гри в мультіплеєр. \n-Ерік",
"googlePlayPurchasesNotAvailableText": "Покупки в Google Play недоступні.\nМожливо, вам знадобиться оновити програму магазину.",
"googlePlayText": "Google Play", "googlePlayText": "Google Play",
"graphicsSettingsWindow": { "graphicsSettingsWindow": {
"alwaysText": "Завжди", "alwaysText": "Завжди",
@ -1109,7 +1113,10 @@
"playlistsText": "Плейлисти", "playlistsText": "Плейлисти",
"pleaseRateText": "Якщо вам подобається гра ${APP_NAME}, будь ласка, подумайте про те,\nщоб оцінити її або написати рецензію. Це забезпечує корисну\nзворотний зв'язок і допомагає підтримати подальшу розробку.\n\nДякуємо!\n-Ерік", "pleaseRateText": "Якщо вам подобається гра ${APP_NAME}, будь ласка, подумайте про те,\nщоб оцінити її або написати рецензію. Це забезпечує корисну\nзворотний зв'язок і допомагає підтримати подальшу розробку.\n\nДякуємо!\n-Ерік",
"pleaseWaitText": "Будь ласка зачекайте...", "pleaseWaitText": "Будь ласка зачекайте...",
"pluginsDetectedText": "Новий плагін(и) виявлені.Ввімкніть/підтвердіть це в налаштуваннях.", "pluginClassLoadErrorText": "Помилка завантаження класу плагіна \"${PLUGIN}\": ${ERROR}",
"pluginInitErrorText": "Помилка запуску плагіна \"${PLUGIN}\": ${ERROR}",
"pluginsDetectedText": "Виявлено нові плагіни. Перезапустіть, щоб активувати їх, або налаштуйте їх у налаштуваннях",
"pluginsRemovedText": "${NUM} плагін(ів) більше не знайдено.",
"pluginsText": "Плагіни", "pluginsText": "Плагіни",
"practiceText": "Тренування", "practiceText": "Тренування",
"pressAnyButtonPlayAgainText": "Натисніть будь-яку кнопку щоб грати знову...", "pressAnyButtonPlayAgainText": "Натисніть будь-яку кнопку щоб грати знову...",
@ -1360,6 +1367,7 @@
"tournamentStandingsText": "Позиції в турнірі", "tournamentStandingsText": "Позиції в турнірі",
"tournamentText": "Турнір", "tournamentText": "Турнір",
"tournamentTimeExpiredText": "Час турніру минув", "tournamentTimeExpiredText": "Час турніру минув",
"tournamentsDisabledWorkspaceText": "Турніри вимкнені, коли робочі області активні.\n Щоб знову ввімкнути турніри, вимкніть робочу область і перезапустіть.",
"tournamentsText": "Турніри", "tournamentsText": "Турніри",
"translations": { "translations": {
"characterNames": { "characterNames": {
@ -1845,6 +1853,8 @@
"winsPlayerText": "Переміг ${NAME}!", "winsPlayerText": "Переміг ${NAME}!",
"winsTeamText": "Перемогли ${NAME}!", "winsTeamText": "Перемогли ${NAME}!",
"winsText": "${NAME} виграв!", "winsText": "${NAME} виграв!",
"workspaceSyncErrorText": "Помилка синхронізації ${WORKSPACE}. Подробиці дивіться в журналі.",
"workspaceSyncReuseText": "Не вдається синхронізувати ${WORKSPACE}. Повторне використання попередньої синхронізованої версії.",
"worldScoresUnavailableText": "Світові результати недоступні.", "worldScoresUnavailableText": "Світові результати недоступні.",
"worldsBestScoresText": "Кращі в світі результати", "worldsBestScoresText": "Кращі в світі результати",
"worldsBestTimesText": "Кращий світовий час", "worldsBestTimesText": "Кращий світовий час",

View file

@ -4,7 +4,7 @@
"accountsText": "Account", "accountsText": "Account",
"achievementProgressText": "Obietivi: ${COUNT} de ${TOTAL}", "achievementProgressText": "Obietivi: ${COUNT} de ${TOTAL}",
"campaignProgressText": "Progreso canpagna [Defìsiłe]: ${PROGRESS}", "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).", "changeOncePerSeasonError": "Par canbiarlo te ghè da spetar ła pròsema stajon (${NUM} days).",
"customName": "Nome parsonałizà", "customName": "Nome parsonałizà",
"linkAccountsEnterCodeText": "Insarisi còdaze", "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?", "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", "resetProgressText": "Ełìmena progresi",
"setAccountName": "Inposta un nome utente", "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.", "signInInfoText": "Conétate par tirar sù biłieti, batajar online e\nsparpagnar i to progresi infrà dispozidivi defarenti.",
"signInText": "Conétate", "signInText": "Conétate",
"signInWithDeviceInfoText": "(par 'sto dispozidivo ze disponìbiłe un soło account automàtego)", "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.", "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}.", "apiVersionErrorText": "Inposìbiłe cargar el mòduło ${NAME}, el se refarise a ła varsion ${VERSION_USED}. Serve invese ła ${VERSION_REQUIRED}.",
"audioSettingsWindow": { "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", "headRelativeVRAudioText": "Àudio par fonarołe VR",
"musicVolumeText": "Vołume mùzega", "musicVolumeText": "Vołume mùzega",
"soundVolumeText": "Vołume son", "soundVolumeText": "Vołume son",
"soundtrackButtonText": "Son de fondo", "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" "titleText": "Àudio"
}, },
"autoText": "Automàtega", "autoText": "Automàtega",
@ -376,7 +376,7 @@
}, },
"configGamepadSelectWindow": { "configGamepadSelectWindow": {
"androidNoteText": "Nota: ła conpatibiłidà par i controładori ła muda drio dispozidivo e varsion de Android.", "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" "titleText": "Configura controładori"
}, },
"configGamepadWindow": { "configGamepadWindow": {
@ -459,7 +459,7 @@
"controlsText": "Comandi", "controlsText": "Comandi",
"coopSelectWindow": { "coopSelectWindow": {
"activenessAllTimeInfoText": "'Sto chive no'l vien aplegà inte ła clasìfega globałe.", "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", "activityText": "Costansa",
"campaignText": "Canpagna", "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.", "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", "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.", "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.", "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:" "yourPowerRankingText": "Ła to pozision:"
}, },
"copyConfirmText": "Copià inte łe note.",
"copyOfText": "Copia de ${NAME}", "copyOfText": "Copia de ${NAME}",
"copyText": "Copia", "copyText": "Copia",
"createEditPlayerText": "<Crea/Muda zugador>", "createEditPlayerText": "<Crea/Muda zugador>",
@ -503,7 +504,7 @@
"additionalAudioArtIdeasText": "Soni adisionałi, gràfega inisiałe e idee de ${NAME}", "additionalAudioArtIdeasText": "Soni adisionałi, gràfega inisiałe e idee de ${NAME}",
"additionalMusicFromText": "Mùzega adisionałe 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", "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:", "languageTranslationsText": "Tradusion inte cheł'altre łengue:",
"legalText": "Informasion łegałi:", "legalText": "Informasion łegałi:",
"publicDomainMusicViaText": "Mùzega a dòparo pùblego de ${NAME}", "publicDomainMusicViaText": "Mùzega a dòparo pùblego de ${NAME}",
@ -586,7 +587,7 @@
"titleEditText": "Muda profiło", "titleEditText": "Muda profiło",
"titleNewText": "Profiło novo", "titleNewText": "Profiło novo",
"unavailableText": "\"${NAME}\" no'l ze miga disponìbiłe: proa n'antro nome.", "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" "upgradeToGlobalProfileText": "Mejora a profiło globałe"
}, },
"editSoundtrackWindow": { "editSoundtrackWindow": {
@ -622,7 +623,9 @@
"epicDescriptionFilterText": "${DESCRIPTION} in movensa camoma.", "epicDescriptionFilterText": "${DESCRIPTION} in movensa camoma.",
"epicNameFilterText": "${NAME} in movensa camoma", "epicNameFilterText": "${NAME} in movensa camoma",
"errorAccessDeniedText": "aceso refudà", "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", "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", "errorText": "Eror",
"errorUnknownText": "eror miga conosesto", "errorUnknownText": "eror miga conosesto",
"exitGameText": "Vutu ndar fora da ${APP_NAME}?", "exitGameText": "Vutu ndar fora da ${APP_NAME}?",
@ -686,7 +689,7 @@
"copyCodeText": "Copia còdaze", "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.", "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?", "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...", "earnTicketsForRecommendingText": "Sparpagna el zugo par\nver biłieti gratùidi...",
"emailItText": "Màndeło par mail", "emailItText": "Màndeło par mail",
"favoritesSaveText": "Salva inte i prefarìì", "favoritesSaveText": "Salva inte i prefarìì",
@ -695,7 +698,7 @@
"freeCloudServerAvailableNowText": "Disponìbiłe server agratis!", "freeCloudServerAvailableNowText": "Disponìbiłe server agratis!",
"freeCloudServerNotAvailableText": "Gnaun server agratis disponìbiłe.", "freeCloudServerNotAvailableText": "Gnaun server agratis disponìbiłe.",
"friendHasSentPromoCodeText": "Par ti ${COUNT} biłieti de ${APP_NAME} da ${NAME}!", "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.", "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.", "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.", "friendPromoCodeRedeemLongText": "El połe èsar scanbià par ${COUNT} biłieti gratùidi da ${MAX_USES} parsone.",
@ -755,7 +758,7 @@
"privateText": "Privà", "privateText": "Privà",
"publicHostRouterConfigText": "Podarìa servir configurar na porta spesìfega so'l to router. Ospidar un grupo privà ze pì fàsiłe.", "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", "publicText": "Pùblego",
"requestingAPromoCodeText": "Drio far domanda de un còdaze...", "requestingAPromoCodeText": "Dimanda par un còdaze…",
"sendDirectInvitesText": "Manda invidi direti", "sendDirectInvitesText": "Manda invidi direti",
"shareThisCodeWithFriendsText": "Sparpagna 'sto còdaze co i amighi:", "shareThisCodeWithFriendsText": "Sparpagna 'sto còdaze co i amighi:",
"showMyAddressText": "Mostra el me ndariso", "showMyAddressText": "Mostra el me ndariso",
@ -776,19 +779,19 @@
"freeText": "GRATIS!", "freeText": "GRATIS!",
"freeTicketsText": "Biłieti gratùidi", "freeTicketsText": "Biłieti gratùidi",
"inProgressText": "Na tranzasion ła ze dezà in ełaborasion: proa danovo infrà na scianta.", "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!", "receivedTicketsText": "${COUNT} biłieti resevesti!",
"restorePurchasesText": "Reprìstena cronpade", "restorePurchasesText": "Recùpara cronpe.",
"ticketPack1Text": "Pacheto de biłieti ceło", "ticketPack1Text": "Pacheto de biłieti ceło",
"ticketPack2Text": "Pacheto de biłieti mezan", "ticketPack2Text": "Pacheto de biłieti mezan",
"ticketPack3Text": "Pacheto de biłieti grando", "ticketPack3Text": "Pacheto de biłieti grando",
"ticketPack4Text": "Pacheto de biłieti ultra", "ticketPack4Text": "Pacheto de biłieti ultra",
"ticketPack5Text": "Pacheto de biłieti despropozità", "ticketPack5Text": "Pacheto de biłieti despropozità",
"ticketPack6Text": "Pacheto de biłieti defenidivo", "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", "ticketsText": "${COUNT} biłieti",
"titleText": "Otien 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.", "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.", "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.", "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" "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", "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", "googlePlayText": "Google Play",
"graphicsSettingsWindow": { "graphicsSettingsWindow": {
"alwaysText": "Senpre", "alwaysText": "Senpre",
@ -1295,7 +1299,7 @@
"charactersText": "Parsonaji", "charactersText": "Parsonaji",
"comingSoonText": "E presto...", "comingSoonText": "E presto...",
"extrasText": "Extra", "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", "holidaySpecialText": "Spesiałe feste",
"howToSwitchCharactersText": "(và so \"${SETTINGS} > ${PLAYER_PROFILES}\" par sernir i to parsonaji e parsonałizarli)", "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)", "howToUseIconsText": "(par dopararle, crea un profiło zugador globałe inte ła sesion account)",
@ -1360,6 +1364,7 @@
"tournamentStandingsText": "Clasìfega tornèo", "tournamentStandingsText": "Clasìfega tornèo",
"tournamentText": "Tornèo", "tournamentText": "Tornèo",
"tournamentTimeExpiredText": "Tenpo de'l tornèo fenì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", "tournamentsText": "Tornèi",
"translations": { "translations": {
"characterNames": { "characterNames": {
@ -1851,7 +1856,7 @@
"xbox360ControllersWindow": { "xbox360ControllersWindow": {
"getDriverText": "Descarga el driver", "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.", "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.", "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}:" "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:", "achievementsRemainingText": "Các thành tựu tiếp theo:",
"achievementsText": "Các thành tựu", "achievementsText": "Các thành tựu",
"achievementsUnavailableForOldSeasonsText": "Xin lỗi , huy hiệu này không có ở mùa trước", "achievementsUnavailableForOldSeasonsText": "Xin lỗi , huy hiệu này không có ở mùa trước",
"activatedText": "${THING} đã được kích hoạt.",
"addGameWindow": { "addGameWindow": {
"getMoreGamesText": "Thêm các thể loại chơi", "getMoreGamesText": "Thêm các thể loại chơi",
"titleText": "Thêm trận đấu" "titleText": "Thêm trận đấu"
@ -627,7 +628,9 @@
"epicDescriptionFilterText": "${DESCRIPTION} trong chế độ quay chậm.", "epicDescriptionFilterText": "${DESCRIPTION} trong chế độ quay chậm.",
"epicNameFilterText": "${NAME} Quay Chậm", "epicNameFilterText": "${NAME} Quay Chậm",
"errorAccessDeniedText": "từ chối kết nối", "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ớ", "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", "errorText": "Lỗi",
"errorUnknownText": "Không rõ lỗi", "errorUnknownText": "Không rõ lỗi",
"exitGameText": "Thoát ${APP_NAME}?", "exitGameText": "Thoát ${APP_NAME}?",
@ -790,7 +793,7 @@
"ticketPack4Text": "Gói vé siêu lớn", "ticketPack4Text": "Gói vé siêu lớn",
"ticketPack5Text": "Gói vé khổng lồ", "ticketPack5Text": "Gói vé khổng lồ",
"ticketPack6Text": "Gói vé siêu 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é", "ticketsText": "${COUNT} Vé",
"titleText": "Lấy 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 ở đó.", "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é" "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", "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", "googlePlayText": "Google Trò chơi",
"graphicsSettingsWindow": { "graphicsSettingsWindow": {
"alwaysText": "Luôn Luôn", "alwaysText": "Luôn Luôn",
@ -1111,7 +1115,10 @@
"playlistsText": "Danh sách", "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", "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ờ...", "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", "pluginsText": "Cắm",
"practiceText": "Luyện tập", "practiceText": "Luyện tập",
"pressAnyButtonPlayAgainText": "Nhấn nút bất kỳ để chơi lại...", "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", "tournamentStandingsText": "Bảng xếp hạng giải đấu",
"tournamentText": "Giải đấu", "tournamentText": "Giải đấu",
"tournamentTimeExpiredText": "Giải đấu đã hết thời gian.", "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", "tournamentsText": "Giải đấu",
"translations": { "translations": {
"characterNames": { "characterNames": {
@ -1845,6 +1853,8 @@
"winsPlayerText": "${NAME} Chiến thắng!", "winsPlayerText": "${NAME} Chiến thắng!",
"winsTeamText": "${NAME} Chiến thắng!", "winsTeamText": "${NAME} Chiến thắng!",
"winsText": "${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.", "worldScoresUnavailableText": "Điểm trên thế giới không có sẵn.",
"worldsBestScoresText": "Điểm số thế giới cao nhất", "worldsBestScoresText": "Điểm số thế giới cao nhất",
"worldsBestTimesText": "Thời gian tốt nhất thế giới", "worldsBestTimesText": "Thời gian tốt nhất thế giới",

View file

@ -1,4 +1,4 @@
from .core import contents, where from .core import contents, where
__all__ = ["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 7W4WAie3AjEA3VoXK3YdZUKWpqxdinlW2Iob35reX8dQj7FbcQwm32pAAOwzkSFx
vmjkI6TZraE3 vmjkI6TZraE3
-----END CERTIFICATE----- -----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. This module returns the installation location of cacert.pem or its contents.
""" """
import os import sys
import types
from typing import Union
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_CTX = None
_CACERT_PATH = None _CACERT_PATH = None
@ -33,13 +33,54 @@ try:
# We also have to hold onto the actual context manager, because # We also have to hold onto the actual context manager, because
# it will do the cleanup whenever it gets garbage collected, so # it will do the cleanup whenever it gets garbage collected, so
# we will also store that at the global level as well. # 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_CTX = get_path("certifi", "cacert.pem")
_CACERT_PATH = str(_CACERT_CTX.__enter__()) _CACERT_PATH = str(_CACERT_CTX.__enter__())
return _CACERT_PATH 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] Package = Union[types.ModuleType, str]
Resource = Union[str, "os.PathLike"] Resource = Union[str, "os.PathLike"]
@ -63,6 +104,5 @@ except ImportError:
return os.path.join(f, "cacert.pem") return os.path.join(f, "cacert.pem")
def contents() -> str:
def contents() -> str: return read_text("certifi", "cacert.pem", encoding="ascii")
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, Node, SessionPlayer, Sound, Texture, Timer, Vec3, Widget, buttonwidget,
camerashake, checkboxwidget, columnwidget, containerwidget, do_once, camerashake, checkboxwidget, columnwidget, containerwidget, do_once,
emitfx, getactivity, getcollidemodel, getmodel, getnodes, getsession, emitfx, getactivity, getcollidemodel, getmodel, getnodes, getsession,
getsound, gettexture, hscrollwidget, imagewidget, log, newactivity, getsound, gettexture, hscrollwidget, imagewidget, newactivity, newnode,
newnode, playsound, printnodes, printobjects, pushcall, quit, rowwidget, playsound, printnodes, printobjects, pushcall, quit, rowwidget, safecolor,
safecolor, screenmessage, scrollwidget, set_analytics_screen, charstr, screenmessage, scrollwidget, set_analytics_screen, charstr, textwidget,
textwidget, time, timer, open_url, widget, clipboard_is_supported, time, timer, open_url, widget, clipboard_is_supported, clipboard_has_text,
clipboard_has_text, clipboard_get_text, clipboard_set_text, getdata) clipboard_get_text, clipboard_set_text, getdata, in_logic_thread)
from ba._activity import Activity from ba._activity import Activity
from ba._plugin import PotentialPlugin, Plugin, PluginSubsystem from ba._plugin import PotentialPlugin, Plugin, PluginSubsystem
from ba._actor import Actor from ba._actor import Actor
@ -99,10 +99,10 @@ __all__ = [
'GameTip', 'garbage_collect', 'getactivity', 'getclass', 'getcollidemodel', 'GameTip', 'garbage_collect', 'getactivity', 'getclass', 'getcollidemodel',
'getcollision', 'getdata', 'getmaps', 'getmodel', 'getnodes', 'getsession', 'getcollision', 'getdata', 'getmaps', 'getmodel', 'getnodes', 'getsession',
'getsound', 'gettexture', 'HitMessage', 'hscrollwidget', 'imagewidget', 'getsound', 'gettexture', 'HitMessage', 'hscrollwidget', 'imagewidget',
'ImpactDamageMessage', 'InputDevice', 'InputDeviceNotFoundError', 'ImpactDamageMessage', 'in_logic_thread', 'InputDevice',
'InputType', 'IntChoiceSetting', 'IntSetting', 'InputDeviceNotFoundError', 'InputType', 'IntChoiceSetting', 'IntSetting',
'is_browser_likely_available', 'is_point_in_box', 'Keyboard', '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', 'MetadataSubsystem', 'Model', 'MultiTeamSession', 'MusicPlayer',
'MusicPlayMode', 'MusicSubsystem', 'MusicType', 'newactivity', 'newnode', 'MusicPlayMode', 'MusicSubsystem', 'MusicType', 'newactivity', 'newnode',
'Node', 'NodeActor', 'NodeNotFoundError', 'normalized_color', 'Node', 'NodeActor', 'NodeNotFoundError', 'normalized_color',

View file

@ -9,6 +9,7 @@ import time
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import _ba import _ba
from ba import _internal
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any from typing import Any
@ -41,7 +42,7 @@ class AccountV1Subsystem:
def do_auto_sign_in() -> None: def do_auto_sign_in() -> None:
if _ba.app.headless_mode or _ba.app.config.get( if _ba.app.headless_mode or _ba.app.config.get(
'Auto Account State') == 'Local': 'Auto Account State') == 'Local':
_ba.sign_in_v1('Local') _internal.sign_in_v1('Local')
_ba.pushcall(do_auto_sign_in) _ba.pushcall(do_auto_sign_in)
@ -108,8 +109,8 @@ class AccountV1Subsystem:
if data['p']: if data['p']:
pro_mult = 1.0 + float( 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 0.0)) * 0.01
else: else:
pro_mult = 1.0 pro_mult = 1.0
@ -135,12 +136,13 @@ class AccountV1Subsystem:
"""(internal)""" """(internal)"""
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
from ba import _store from ba import _store
if _ba.get_v1_account_state() != 'signed_in': if _internal.get_v1_account_state() != 'signed_in':
return [] return []
icons = [] icons = []
store_items = _store.get_store_items() store_items = _store.get_store_items()
for item_name, item in list(store_items.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']) icons.append(item['icon'])
return icons return icons
@ -152,12 +154,13 @@ class AccountV1Subsystem:
(internal) (internal)
""" """
# This only applies when we're signed in. # 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 return
# If the short version of our account name currently cant be # If the short version of our account name currently cant be
# displayed by the game, cancel. # 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 return
config = _ba.app.config config = _ba.app.config
@ -165,7 +168,7 @@ class AccountV1Subsystem:
or '__account__' not in config['Player Profiles']): or '__account__' not in config['Player Profiles']):
# Create a spaz with a nice default purply color. # Create a spaz with a nice default purply color.
_ba.add_transaction({ _internal.add_transaction({
'type': 'ADD_PLAYER_PROFILE', 'type': 'ADD_PLAYER_PROFILE',
'name': '__account__', 'name': '__account__',
'profile': { 'profile': {
@ -174,7 +177,7 @@ class AccountV1Subsystem:
'highlight': [0.5, 0.25, 1.0] 'highlight': [0.5, 0.25, 1.0]
} }
}) })
_ba.run_transactions() _internal.run_transactions()
def have_pro(self) -> bool: def have_pro(self) -> bool:
"""Return whether pro is currently unlocked.""" """Return whether pro is currently unlocked."""
@ -182,9 +185,9 @@ class AccountV1Subsystem:
# Check our tickets-based pro upgrade and our two real-IAP based # Check our tickets-based pro upgrade and our two real-IAP based
# upgrades. Also always unlock this stuff in ballistica-core builds. # upgrades. Also always unlock this stuff in ballistica-core builds.
return bool( return bool(
_ba.get_purchased('upgrades.pro') _internal.get_purchased('upgrades.pro')
or _ba.get_purchased('static.pro') or _internal.get_purchased('static.pro')
or _ba.get_purchased('static.pro_sale') or _internal.get_purchased('static.pro_sale')
or 'ballistica' + 'core' == _ba.appname()) or 'ballistica' + 'core' == _ba.appname())
def have_pro_options(self) -> bool: 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 # or also if we've been grandfathered in or are using ballistica-core
# builds. # builds.
return self.have_pro() or bool( 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) or _ba.app.config.get('lc14292', 0) > 1)
def show_post_purchase_message(self) -> None: def show_post_purchase_message(self) -> None:
@ -221,17 +225,17 @@ class AccountV1Subsystem:
from ba._language import Lstr from ba._language import Lstr
# Run any pending promo codes we had queued up while not signed in. # 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: ) == 'signed_in' and self.pending_promo_codes:
for code in self.pending_promo_codes: for code in self.pending_promo_codes:
_ba.screenmessage(Lstr(resource='submittingPromoCodeText'), _ba.screenmessage(Lstr(resource='submittingPromoCodeText'),
color=(0, 1, 0)) color=(0, 1, 0))
_ba.add_transaction({ _internal.add_transaction({
'type': 'PROMO_CODE', 'type': 'PROMO_CODE',
'expire_time': time.time() + 5, 'expire_time': time.time() + 5,
'code': code 'code': code
}) })
_ba.run_transactions() _internal.run_transactions()
self.pending_promo_codes = [] self.pending_promo_codes = []
def add_pending_promo_code(self, code: str) -> None: 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 # 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 # are and issue a warning if we haven't signed in within the next
# few seconds. # few seconds.
if _ba.get_v1_account_state() != 'signed_in': if _internal.get_v1_account_state() != 'signed_in':
def check_pending_codes() -> None: def check_pending_codes() -> None:
"""(internal)""" """(internal)"""
@ -259,9 +263,9 @@ class AccountV1Subsystem:
return return
_ba.screenmessage(Lstr(resource='submittingPromoCodeText'), _ba.screenmessage(Lstr(resource='submittingPromoCodeText'),
color=(0, 1, 0)) color=(0, 1, 0))
_ba.add_transaction({ _internal.add_transaction({
'type': 'PROMO_CODE', 'type': 'PROMO_CODE',
'expire_time': time.time() + 5, 'expire_time': time.time() + 5,
'code': code 'code': code
}) })
_ba.run_transactions() _internal.run_transactions()

View file

@ -6,6 +6,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import _ba import _ba
from ba import _internal
from ba._error import print_exception from ba._error import print_exception
if TYPE_CHECKING: if TYPE_CHECKING:
@ -317,10 +318,13 @@ class AchievementSubsystem:
if not ach.complete: if not ach.complete:
# Report new achievements to the game-service. # Report new achievements to the game-service.
_ba.report_achievement(achname) _internal.report_achievement(achname)
# And to our account. # And to our account.
_ba.add_transaction({'type': 'ACHIEVEMENT', 'name': achname}) _internal.add_transaction({
'type': 'ACHIEVEMENT',
'name': achname
})
# Now attempt to show a banner. # Now attempt to show a banner.
self.display_achievement_banner(achname) 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) (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) assert isinstance(val, int)
if include_pro_bonus and _ba.app.accounts_v1.have_pro(): if include_pro_bonus and _ba.app.accounts_v1.have_pro():
val *= 2 val *= 2
@ -496,7 +500,7 @@ class Achievement:
# signed in, lets not show them (otherwise we tend to get # signed in, lets not show them (otherwise we tend to get
# confusing 'controller connected' achievements popping up while # confusing 'controller connected' achievements popping up while
# waiting to log in which can be confusing). # 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 return
# If we're being freshly complete, display/report it and whatnot. # 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: def get_award_ticket_value(self, include_pro_bonus: bool = False) -> int:
"""Get the ticket award value for this achievement.""" """Get the ticket award value for this achievement."""
val: int = (_ba.get_v1_account_misc_read_val('achAward.' + self._name, val: int = (_internal.get_v1_account_misc_read_val(
self._award) * 'achAward.' + self._name, self._award) *
_get_ach_mult(include_pro_bonus)) _get_ach_mult(include_pro_bonus))
assert isinstance(val, int) assert isinstance(val, int)
return val return val
@ -601,7 +605,7 @@ class Achievement:
@property @property
def power_ranking_value(self) -> int: def power_ranking_value(self) -> int:
"""Get the power-ranking award value for this achievement.""" """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) 'achLeaguePoints.' + self._name, self._award)
assert isinstance(val, int) assert isinstance(val, int)
return val return val

View file

@ -15,7 +15,7 @@ if TYPE_CHECKING:
from typing import Any, Literal from typing import Any, Literal
import ba import ba
TA = TypeVar('TA', bound='Actor') ActorT = TypeVar('ActorT', bound='Actor')
class Actor: class Actor:
@ -95,7 +95,7 @@ class Actor:
return UNHANDLED return UNHANDLED
def autoretain(self: TA) -> TA: def autoretain(self: ActorT) -> ActorT:
"""Keep this Actor alive without needing to hold a reference to it. """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 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 from typing import TYPE_CHECKING
import _ba import _ba
from ba import _internal
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Callable, Any from typing import Callable, Any
@ -94,15 +95,15 @@ class AdsSubsystem:
launch_count = app.config.get('launchCount', 0) launch_count = app.config.get('launchCount', 0)
# If we're seeing short ads we may want to space them differently. # 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) 'ads.shortIntervalMult', 1.0)
if self.last_ad_was_short else 1.0) if self.last_ad_was_short else 1.0)
if self.ad_amt is None: if self.ad_amt is None:
if launch_count <= 1: 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) 'ads.startVal1', 0.99)
else: 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) 'ads.startVal2', 1.0)
interval = None interval = None
else: else:
@ -111,15 +112,17 @@ class AdsSubsystem:
# (we reach our threshold faster the longer we've been # (we reach our threshold faster the longer we've been
# playing). # playing).
base = 'ads' if _ba.has_video_ads() else 'ads2' base = 'ads' if _ba.has_video_ads() else 'ads2'
min_lc = _ba.get_v1_account_misc_read_val(base + '.minLC', 0.0) min_lc = _internal.get_v1_account_misc_read_val(
max_lc = _ba.get_v1_account_misc_read_val(base + '.maxLC', 5.0) base + '.minLC', 0.0)
min_lc_scale = (_ba.get_v1_account_misc_read_val( 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)) 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)) 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)) 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)) base + '.maxLCInterval', 300))
if launch_count < min_lc: if launch_count < min_lc:
lc_amt = 0.0 lc_amt = 0.0

View file

@ -20,11 +20,13 @@ from ba._meta import MetadataSubsystem
from ba._ads import AdsSubsystem from ba._ads import AdsSubsystem
from ba._net import NetworkSubsystem from ba._net import NetworkSubsystem
from ba._workspace import WorkspaceSubsystem from ba._workspace import WorkspaceSubsystem
from ba import _internal
if TYPE_CHECKING: if TYPE_CHECKING:
import asyncio import asyncio
from typing import Any, Callable from typing import Any, Callable
import efro.log
import ba import ba
from ba._cloud import CloudSubsystem from ba._cloud import CloudSubsystem
from bastd.actor import spazappearance from bastd.actor import spazappearance
@ -48,6 +50,7 @@ class App:
# Implementations for these will be filled in by internal libs. # Implementations for these will be filled in by internal libs.
accounts_v2: AccountV2Subsystem accounts_v2: AccountV2Subsystem
cloud: CloudSubsystem cloud: CloudSubsystem
log_handler: efro.log.LogHandler
class State(Enum): class State(Enum):
"""High level state the app can be in.""" """High level state the app can be in."""
@ -91,6 +94,12 @@ class App:
assert isinstance(self._env['build_number'], int) assert isinstance(self._env['build_number'], int)
return self._env['build_number'] 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 @property
def config_file_path(self) -> str: def config_file_path(self) -> str:
"""Where the game's config file is stored on disk.""" """Where the game's config file is stored on disk."""
@ -223,6 +232,7 @@ class App:
self._launch_completed = False self._launch_completed = False
self._initial_login_completed = False self._initial_login_completed = False
self._meta_scan_completed = False
self._called_on_app_running = False self._called_on_app_running = False
self._app_paused = False self._app_paused = False
@ -344,6 +354,7 @@ class App:
from bastd.actor import spazappearance from bastd.actor import spazappearance
from ba._generated.enums import TimeType from ba._generated.enums import TimeType
assert _ba.in_logic_thread()
self._aioloop = _asyncio.setup_asyncio() self._aioloop = _asyncio.setup_asyncio()
@ -370,12 +381,12 @@ class App:
# Non-test, non-debug builds should generally be blessed; warn if not. # 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) # (so I don't accidentally release a build that can't play tourneys)
if (not self.debug_build and not self.test_build 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)) _ba.screenmessage('WARNING: NON-BLESSED BUILD', color=(1, 0, 0))
# If there's a leftover log file, attempt to upload it to the # If there's a leftover log file, attempt to upload it to the
# master-server and/or get rid of it. # 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 # Only do this stuff if our config file is healthy so we don't
# overwrite a broken one or whatnot and wipe out data. # overwrite a broken one or whatnot and wipe out data.
@ -408,7 +419,8 @@ class App:
def check_special_offer() -> None: def check_special_offer() -> None:
from bastd.ui.specialoffer import show_offer from bastd.ui.specialoffer import show_offer
config = self.config 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']): == config['pendingSpecialOffer']['a']):
self.special_offer = config['pendingSpecialOffer']['o'] self.special_offer = config['pendingSpecialOffer']['o']
show_offer() show_offer()
@ -416,6 +428,9 @@ class App:
if not self.headless_mode: if not self.headless_mode:
_ba.timer(3.0, check_special_offer, timetype=TimeType.REAL) _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_v2.on_app_launch()
self.accounts_v1.on_app_launch() self.accounts_v1.on_app_launch()
@ -430,17 +445,27 @@ class App:
def on_app_running(self) -> None: def on_app_running(self) -> None:
"""Called when initially entering the running state.""" """Called when initially entering the running state."""
self.meta.on_app_running()
self.plugins.on_app_running() self.plugins.on_app_running()
# from ba._dependency import test_depset # from ba._dependency import test_depset
# 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: def _update_state(self) -> None:
assert _ba.in_logic_thread()
if self._app_paused: if self._app_paused:
self.state = self.State.PAUSED self.state = self.State.PAUSED
else: else:
if self._initial_login_completed: if self._initial_login_completed and self._meta_scan_completed:
self.state = self.State.RUNNING self.state = self.State.RUNNING
if not self._called_on_app_running: if not self._called_on_app_running:
self._called_on_app_running = True self._called_on_app_running = True
@ -562,11 +587,11 @@ class App:
# Kick off a little transaction so we'll hopefully have all the # Kick off a little transaction so we'll hopefully have all the
# latest account state when we get back to the menu. # latest account state when we get back to the menu.
_ba.add_transaction({ _internal.add_transaction({
'type': 'END_SESSION', 'type': 'END_SESSION',
'sType': str(type(host_session)) 'sType': str(type(host_session))
}) })
_ba.run_transactions() _internal.run_transactions()
host_session.end() host_session.end()
@ -651,5 +676,9 @@ class App:
This should also run after a short amount of time if no login This should also run after a short amount of time if no login
has occurred. 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._initial_login_completed = True
self._update_state() self._update_state()

View file

@ -128,12 +128,6 @@ def read_config() -> tuple[AppConfig, bool]:
shutil.copyfile(config_file_path, config_file_path + '.broken') shutil.copyfile(config_file_path, config_file_path + '.broken')
except Exception as exc2: except Exception as exc2:
print('EXC copying broken config:', 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() config = AppConfig()
# Now attempt to read one of our 'prev' backup copies. # Now attempt to read one of our 'prev' backup copies.
@ -159,8 +153,9 @@ def commit_app_config(force: bool = False) -> None:
(internal) (internal)
""" """
from ba._internal import mark_config_dirty
if not _ba.app.config_file_healthy and not force: if not _ba.app.config_file_healthy and not force:
print('Current config file is broken; ' print('Current config file is broken; '
'skipping write to avoid losing settings.') 'skipping write to avoid losing settings.')
return 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) return _ba.app.config.get('Submit Debug Info', True)
def handle_log() -> None: def handle_v1_cloud_log() -> None:
"""Called on debug log prints. """Called on debug log prints.
When this happens, we can upload our log to the server 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._net import master_server_post
from ba._generated.enums import TimeType from ba._generated.enums import TimeType
from ba._internal import get_news_show
app = _ba.app app = _ba.app
app.log_have_new = True app.log_have_new = True
if not app.log_upload_timer_started: if not app.log_upload_timer_started:
@ -73,7 +74,7 @@ def handle_log() -> None:
activityname = 'unavailable' activityname = 'unavailable'
info = { info = {
'log': _ba.getlog(), 'log': _ba.get_v1_cloud_log(),
'version': app.version, 'version': app.version,
'build': app.build_number, 'build': app.build_number,
'userAgentString': app.user_agent_string, 'userAgentString': app.user_agent_string,
@ -82,8 +83,8 @@ def handle_log() -> None:
'fatal': 0, 'fatal': 0,
'userRanCommands': _ba.has_user_run_commands(), 'userRanCommands': _ba.has_user_run_commands(),
'time': _ba.time(TimeType.REAL), 'time': _ba.time(TimeType.REAL),
'userModded': _ba.has_user_mods(), 'userModded': _ba.workspaces_in_use(),
'newsShow': _ba.get_news_show(), 'newsShow': get_news_show(),
} }
def response(data: Any) -> None: def response(data: Any) -> None:
@ -107,7 +108,7 @@ def handle_log() -> None:
def _reset() -> None: def _reset() -> None:
app.log_upload_timer_started = False app.log_upload_timer_started = False
if app.log_have_new: if app.log_have_new:
handle_log() handle_v1_cloud_log()
if not _ba.is_log_full(): if not _ba.is_log_full():
with _ba.Context('ui'): with _ba.Context('ui'):
@ -117,14 +118,15 @@ def handle_log() -> None:
suppress_format_warning=True) suppress_format_warning=True)
def handle_leftover_log_file() -> None: def handle_leftover_v1_cloud_log_file() -> None:
"""Handle an un-uploaded log from a previous run.""" """Handle an un-uploaded v1-cloud-log from a previous run."""
try: try:
import json import json
from ba._net import master_server_post from ba._net import master_server_post
if os.path.exists(_ba.get_log_file_path()): if os.path.exists(_ba.get_v1_cloud_log_file_path()):
with open(_ba.get_log_file_path(), encoding='utf-8') as infile: with open(_ba.get_v1_cloud_log_file_path(),
encoding='utf-8') as infile:
info = json.loads(infile.read()) info = json.loads(infile.read())
infile.close() infile.close()
do_send = should_submit_debug_info() do_send = should_submit_debug_info()
@ -135,7 +137,7 @@ def handle_leftover_log_file() -> None:
# lets kill it. # lets kill it.
if data is not None: if data is not None:
try: try:
os.remove(_ba.get_log_file_path()) os.remove(_ba.get_v1_cloud_log_file_path())
except FileNotFoundError: except FileNotFoundError:
# Saw this in the wild. The file just existed # Saw this in the wild. The file just existed
# a moment ago but I suppose something could have # 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) master_server_post('bsLog', info, response)
else: else:
# If they don't want logs uploaded just kill it. # 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: except Exception:
from ba import _error from ba import _error
_error.print_exception('Error handling leftover log file.') _error.print_exception('Error handling leftover log file.')

View file

@ -18,7 +18,7 @@ import os
if TYPE_CHECKING: if TYPE_CHECKING:
import ba 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_timer: ba.Timer | None = None
_asyncio_event_loop: asyncio.AbstractEventLoop | None = None _asyncio_event_loop: asyncio.AbstractEventLoop | None = None
@ -33,7 +33,7 @@ def setup_asyncio() -> asyncio.AbstractEventLoop:
import ba import ba
from ba._generated.enums import TimeType 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 # Create our event-loop. We don't expect there to be one
# running on this thread before we do. # 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. Must be called from a background thread.
""" """
raise RuntimeError('Cloud functionality is not available.') 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 from typing import TYPE_CHECKING, TypeVar
import _ba import _ba
from ba import _internal
from ba._gameactivity import GameActivity from ba._gameactivity import GameActivity
from ba._general import WeakCall from ba._general import WeakCall
@ -54,19 +55,6 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
# Preload achievement images in case we get some. # Preload achievement images in case we get some.
_ba.timer(2.0, WeakCall(self._preload_achievements)) _ba.timer(2.0, WeakCall(self._preload_achievements))
# Let's ask the server for a 'time-to-beat' value.
levelname = self._get_coop_level_name()
campaign = self.session.campaign
assert campaign is not None
config_str = (str(len(self.players)) + 'p' + campaign.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, def _show_standard_scores_to_beat_ui(self,
scores: list[dict[str, Any]]) -> None: scores: list[dict[str, Any]]) -> None:
from efro.util import asserttype from efro.util import asserttype
@ -217,10 +205,10 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
self._achievements_awarded.add(achievement_name) self._achievements_awarded.add(achievement_name)
# Report new achievements to the game-service. # Report new achievements to the game-service.
_ba.report_achievement(achievement_name) _internal.report_achievement(achievement_name)
# ...and to our account. # ...and to our account.
_ba.add_transaction({ _internal.add_transaction({
'type': 'ACHIEVEMENT', 'type': 'ACHIEVEMENT',
'name': achievement_name '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: def print_exception(*args: Any, **keywds: Any) -> None:
"""Print info about an exception along with pertinent context state. """Print info about an exception along with pertinent context state.

View file

@ -9,6 +9,7 @@ import random
from typing import TYPE_CHECKING, TypeVar from typing import TYPE_CHECKING, TypeVar
import _ba import _ba
from ba import _internal
from ba._activity import Activity from ba._activity import Activity
from ba._score import ScoreConfig from ba._score import ScoreConfig
from ba._language import Lstr 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._general import Call, WeakCall
from ba._player import PlayerInfo from ba._player import PlayerInfo
from ba import _map from ba import _map
from ba import _store
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any, Callable, Sequence from typing import Any, Callable, Sequence
@ -239,11 +241,11 @@ class GameActivity(Activity[PlayerType, TeamType]):
self._zoom_message_times: dict[int, float] = {} self._zoom_message_times: dict[int, float] = {}
self._is_waiting_for_continue = False 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) '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) '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) 'continuesOffset', 0)
@property @property
@ -363,11 +365,11 @@ class GameActivity(Activity[PlayerType, TeamType]):
if do_continue: if do_continue:
_ba.playsound(_ba.getsound('shieldUp')) _ba.playsound(_ba.getsound('shieldUp'))
_ba.playsound(_ba.getsound('cashRegister')) _ba.playsound(_ba.getsound('cashRegister'))
_ba.add_transaction({ _internal.add_transaction({
'type': 'CONTINUE', 'type': 'CONTINUE',
'cost': self._continue_cost 'cost': self._continue_cost
}) })
_ba.run_transactions() _internal.run_transactions()
self._continue_cost = ( self._continue_cost = (
self._continue_cost * self._continue_cost_mult + self._continue_cost * self._continue_cost_mult +
self._continue_cost_offset) self._continue_cost_offset)
@ -390,7 +392,8 @@ class GameActivity(Activity[PlayerType, TeamType]):
from ba._generated.enums import TimeType from ba._generated.enums import TimeType
try: try:
if _ba.get_v1_account_misc_read_val('enableContinues', False): if _internal.get_v1_account_misc_read_val('enableContinues',
False):
session = self.session session = self.session
# We only support continuing in non-tournament games. # We only support continuing in non-tournament games.
@ -453,7 +456,7 @@ class GameActivity(Activity[PlayerType, TeamType]):
# time is left. # time is left.
tournament_id = self.session.tournament_id tournament_id = self.session.tournament_id
if tournament_id is not None: if tournament_id is not None:
_ba.tournament_query( _internal.tournament_query(
args={ args={
'tournamentIDs': [tournament_id], 'tournamentIDs': [tournament_id],
'source': 'in-game time remaining query' 'source': 'in-game time remaining query'
@ -1159,7 +1162,7 @@ class GameActivity(Activity[PlayerType, TeamType]):
else: else:
# If settings doesn't specify a map, pick a random one from the # If settings doesn't specify a map, pick a random one from the
# list of supported ones. # list of supported ones.
unowned_maps = _map.get_unowned_maps() unowned_maps = _store.get_unowned_maps()
valid_maps: list[str] = [ valid_maps: list[str] = [
m for m in self.get_supported_maps(type(self.session)) m for m in self.get_supported_maps(type(self.session))
if m not in unowned_maps if m not in unowned_maps

View file

@ -31,13 +31,11 @@ class Existable(Protocol):
"""Whether this object exists.""" """Whether this object exists."""
# pylint: disable=invalid-name ExistableT = TypeVar('ExistableT', bound=Existable)
ExistableType = TypeVar('ExistableType', bound=Existable)
# pylint: enable=invalid-name
T = TypeVar('T') 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. """Convert invalid references to None for any ba.Existable object.
Category: **Gameplay Functions** Category: **Gameplay Functions**
@ -251,6 +249,10 @@ class _Call:
if TYPE_CHECKING: 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 WeakCall = Call
Call = Call Call = Call
else: else:

View file

@ -16,6 +16,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import _ba import _ba
from ba import _internal
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Sequence, Any from typing import Sequence, Any
@ -24,10 +25,10 @@ if TYPE_CHECKING:
def finish_bootstrapping() -> None: def finish_bootstrapping() -> None:
"""Do final bootstrapping related bits.""" """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 # 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() # setup_asyncio()
# Ok, bootstrapping is done; time to get the show started. # 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: def submit_analytics_counts(sval: str) -> None:
_ba.add_transaction({'type': 'ANALYTICS_COUNTS', 'values': sval}) _internal.add_transaction({'type': 'ANALYTICS_COUNTS', 'values': sval})
_ba.run_transactions() _internal.run_transactions()
def set_last_ad_network(sval: str) -> None: def set_last_ad_network(sval: str) -> None:

View file

@ -6,6 +6,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import _ba import _ba
from ba._internal import get_v1_account_display_string
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any from typing import Any
@ -639,5 +640,5 @@ def get_last_player_name_from_input_device(device: ba.InputDevice) -> str:
if profilename == '_random': if profilename == '_random':
profilename = device.get_default_player_name() profilename = device.get_default_player_name()
if profilename == '__account__': if profilename == '__account__':
profilename = _ba.get_v1_account_display_string() profilename = get_v1_account_display_string()
return profilename 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()) 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]: def get_map_class(name: str) -> type[ba.Map]:
"""Return a map type given a name. """Return a map type given a name.

View file

@ -6,33 +6,50 @@ from __future__ import annotations
import os import os
import time import time
import threading import logging
from threading import Thread
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, TypeVar
from dataclasses import dataclass, field from dataclasses import dataclass, field
from efro.call import tpartial
import _ba import _ba
if TYPE_CHECKING: if TYPE_CHECKING:
import ba from typing import Callable
# The meta api version of this build of the game. # The meta api version of this build of the game.
# Only packages and modules requiring this exact api version # Only packages and modules requiring this exact api version
# will be considered when scanning directories. # will be considered when scanning directories.
# See: https://ballistica.net/wiki/Meta-Tags # See: https://ballistica.net/wiki/Meta-Tags
CURRENT_API_VERSION = 6 #TODO update it to latest 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 # 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 # 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 @dataclass
class ScanResults: class ScanResults:
"""Final results from a metadata scan.""" """Final results from a meta-scan."""
games: list[str] = field(default_factory=list) exports: dict[str, list[str]] = field(default_factory=dict)
plugins: list[str] = field(default_factory=list) errors: list[str] = field(default_factory=list)
keyboards: list[str] = field(default_factory=list) warnings: list[str] = field(default_factory=list)
errors: str = ''
warnings: str = '' 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: class MetadataSubsystem:
@ -44,99 +61,98 @@ class MetadataSubsystem:
""" """
def __init__(self) -> None: 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] = [] self.extra_scan_dirs: list[str] = []
def on_app_running(self) -> None: # Results populated once scan is complete.
"""Should be called when the app enters the running state.""" self.scanresults: ScanResults | None = None
# Start scanning for things exposed via ba_meta. self._scan_complete_cb: Callable[[], None] | None = None
self.start_scan()
def start_scan(self) -> None: def start_scan(self, scan_complete_cb: Callable[[], None]) -> None:
"""Begin scanning script directories for scripts containing metadata. """Begin the overall scan.
Should be called only once at launch.""" This will start scanning built in directories (which for vanilla
app = _ba.app installs should be the vast majority of the work). This should only
if self.scanresults is not None: be called once.
print('WARNING: meta scan run more than once.') """
pythondirs = ([app.python_directory_app, app.python_directory_user] + assert self._scan_complete_cb is None
self.extra_scan_dirs) assert self._scan is None
thread = ScanThread(pythondirs)
thread.start()
def handle_scan_results(self, results: ScanResults) -> None: self._scan_complete_cb = scan_complete_cb
"""Called in the game thread with results of a completed scan.""" self._scan = DirectoryScan(
[_ba.app.python_directory_app, _ba.app.python_directory_user])
from ba._language import Lstr Thread(target=self._run_scan_in_bg, daemon=True).start()
from ba._plugin import PotentialPlugin
# Warnings generally only get printed locally for users' benefit def start_extra_scan(self) -> None:
# (things like out-of-date scripts being ignored, etc.) """Proceed to the extra_scan_dirs portion of the scan.
# Errors are more serious and will get included in the regular log
# warnings = results.get('warnings', '')
# errors = results.get('errors', '')
if results.warnings != '' or results.errors != '':
import textwrap
_ba.screenmessage(Lstr(resource='scanScriptsErrorText'),
color=(1, 0, 0))
_ba.playsound(_ba.getsound('error'))
if results.warnings != '':
_ba.log(textwrap.indent(results.warnings,
'Warning (meta-scan): '),
to_server=False)
if results.errors != '':
_ba.log(textwrap.indent(results.errors, 'Error (meta-scan): '))
# Handle plugins. This is for parts of the scan that must be delayed until
plugs = _ba.app.plugins workspace sync completion or other such events. This must be
config_changed = False called exactly once.
found_new = False """
plugstates: dict[str, dict] = _ba.app.config.setdefault('Plugins', {}) assert self._scan is not None
assert isinstance(plugstates, dict) self._scan.set_extras(self.extra_scan_dirs)
# Create a potential-plugin for each class we found in the scan. def load_exported_classes(
for class_path in results.plugins: self,
plugs.potential_plugins.append( cls: type[T],
PotentialPlugin(display_name=Lstr(value=class_path), completion_cb: Callable[[list[type[T]]], None],
class_path=class_path, completion_cb_in_bg_thread: bool = False,
available=True)) ) -> None:
if class_path not in plugstates: """High level function to load meta-exported classes.
# 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
# Also add a special one for any plugins set to load but *not* found Will wait for scanning to complete if necessary, and will load all
# in the scan (this way they will show up in the UI so we can disable registered classes of a particular type in a background thread before
# them) calling the passed callback in the logic thread. Errors may be logged
for class_path, plugstate in plugstates.items(): to messaged to the user in some way but the callback will be called
enabled = plugstate.get('enabled', False) regardless.
assert isinstance(enabled, bool) To run the completion callback directly in the bg thread where the
if enabled and class_path not in results.plugins: loading work happens, pass completion_cb_in_bg_thread=True.
plugs.potential_plugins.append( """
PotentialPlugin(display_name=Lstr(value=class_path), Thread(
class_path=class_path, target=tpartial(self._load_exported_classes, cls, completion_cb,
available=False)) 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: except Exception:
_ba.screenmessage(Lstr(resource='pluginsDetectedText'), logging.exception('Error loading exported classes.')
color=(0, 1, 0))
_ba.playsound(_ba.getsound('ding'))
if config_changed: completion_call = tpartial(completion_cb, classes)
_ba.app.config.commit() if completion_cb_in_bg_thread:
completion_call()
else:
_ba.pushcall(completion_call, from_other_thread=True)
def get_scan_results(self) -> ScanResults: def _wait_for_scan_results(self) -> ScanResults:
"""Return meta scan results; block if the scan is not yet complete.""" """Return scan results, blocking if the scan is not yet complete."""
if self.scanresults is None: if self.scanresults is None:
print('WARNING: ba.meta.get_scan_results()' if _ba.in_logic_thread():
' called before scan completed.' logging.warning(
' This can cause hitches.') '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. # Now wait a bit for the scan to complete.
# Eventually error though if it doesn't. # Eventually error though if it doesn't.
@ -148,69 +164,53 @@ class MetadataSubsystem:
'timeout waiting for meta scan to complete.') 'timeout waiting for meta scan to complete.')
return self.scanresults return self.scanresults
def get_game_types(self) -> list[type[ba.GameActivity]]: def _run_scan_in_bg(self) -> None:
"""Return available game types.""" """Runs a scan (for use in background thread)."""
from ba._general import getclass
from ba._gameactivity import GameActivity
gameclassnames = self.get_scan_results().games
gameclasses = []
for gameclassname in gameclassnames:
try:
cls = getclass(gameclassname, GameActivity)
gameclasses.append(cls)
except Exception:
from ba import _error
_error.print_exception('error importing ' + str(gameclassname))
unowned = self.get_unowned_game_types()
return [cls for cls in gameclasses if cls not in unowned]
def get_unowned_game_types(self) -> set[type[ba.GameActivity]]:
"""Return present game types not owned by the current account."""
try: try:
from ba import _store assert self._scan is not None
unowned_games: set[type[ba.GameActivity]] = set() self._scan.run()
if not _ba.app.headless_mode: results = self._scan.results
for section in _store.get_store_layout()['minigames']: self._scan = None
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
except Exception as exc: 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 # Place results and tell the logic thread they're ready.
# or otherwise deal with scan results. self.scanresults = results
_ba.pushcall(Call(_ba.app.meta.handle_scan_results, results), _ba.pushcall(self._handle_scan_results, from_other_thread=True)
from_other_thread=True)
# We also, however, immediately make results available. def _handle_scan_results(self) -> None:
# This is because the game thread may be blocked waiting """Called in the logic thread with results of a completed scan."""
# for them so we can't push a call or we'd get deadlock. from ba._language import Lstr
_ba.app.meta.scanresults = results 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: class DirectoryScan:
"""Handles scanning directories for metadata.""" """Scans directories for metadata."""
def __init__(self, paths: list[str]): def __init__(self, paths: list[str]):
"""Given one or more paths, parses available meta information. """Given one or more paths, parses available meta information.
@ -220,9 +220,42 @@ class DirectoryScan:
""" """
# Skip non-existent paths completely. # 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() 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, def _get_path_module_entries(self, path: Path, subpath: str | Path,
modules: list[tuple[Path, Path]]) -> None: modules: list[tuple[Path, Path]]) -> None:
"""Scan provided path and add module entries to provided list.""" """Scan provided path and add module entries to provided list."""
@ -237,7 +270,7 @@ class DirectoryScan:
entries = [] entries = []
except Exception as exc: except Exception as exc:
# Unexpected; report this. # Unexpected; report this.
self.results.errors += f'{exc}\n' self.results.errors.append(str(exc))
entries = [] entries = []
# Now identify python packages/modules out of what we found. # 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()): and Path(entry[0], entry[1], '__init__.py').is_file()):
modules.append(entry) modules.append(entry)
def scan(self) -> None: def _scan_module(self, moduledir: Path, subpath: Path) -> 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:
"""Scan an individual module and add the findings to results.""" """Scan an individual module and add the findings to results."""
if subpath.name.endswith('.py'): if subpath.name.endswith('.py'):
fpath = Path(moduledir, subpath) fpath = Path(moduledir, subpath)
@ -279,11 +295,12 @@ class DirectoryScan:
lnum: l[1:].split() lnum: l[1:].split()
for lnum, l in enumerate(flines) if '# ba_meta ' in l for lnum, l in enumerate(flines) if '# ba_meta ' in l
} }
toplevel = len(subpath.parts) <= 1 is_top_level = len(subpath.parts) <= 1
required_api = self.get_api_requirement(subpath, meta_lines, toplevel) required_api = self._get_api_requirement(subpath, meta_lines,
is_top_level)
# Top level modules with no discernible api version get ignored. # 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 return
# If we find a module requiring a different api version, warn # 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: if required_api is not None and required_api < CURRENT_API_VERSION:
self.results.warnings += ( self.results.warnings += (
f'Warning: {subpath} requires api {required_api} but' 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 return
# Ok; can proceed with a full scan of this module. # Ok; can proceed with a full scan of this module.
@ -304,11 +321,11 @@ class DirectoryScan:
self._get_path_module_entries(moduledir, subpath, submodules) self._get_path_module_entries(moduledir, subpath, submodules)
for submodule in submodules: for submodule in submodules:
if submodule[1].name != '__init__.py': if submodule[1].name != '__init__.py':
self.scan_module(submodule[0], submodule[1]) self._scan_module(submodule[0], submodule[1])
except Exception: except Exception:
import traceback import traceback
self.results.warnings += ( self.results.warnings.append(
f"Error scanning '{subpath}': {traceback.format_exc()}\n") f"Error scanning '{subpath}': {traceback.format_exc()}")
def _process_module_meta_tags(self, subpath: Path, flines: list[str], def _process_module_meta_tags(self, subpath: Path, flines: list[str],
meta_lines: dict[int, list[str]]) -> None: meta_lines: dict[int, list[str]]) -> None:
@ -317,10 +334,9 @@ class DirectoryScan:
# meta_lines is just anything containing '# ba_meta '; make sure # meta_lines is just anything containing '# ba_meta '; make sure
# the ba_meta is in the right place. # the ba_meta is in the right place.
if mline[0] != 'ba_meta': if mline[0] != 'ba_meta':
self.results.warnings += ( self.results.warnings.append(
'Warning: ' + str(subpath) + f'Warning: {subpath}:'
': malformed ba_meta statement on line ' + f' malformed ba_meta statement on line {lindex + 1}.')
str(lindex + 1) + '.\n')
elif (len(mline) == 4 and mline[1] == 'require' elif (len(mline) == 4 and mline[1] == 'require'
and mline[2] == 'api'): and mline[2] == 'api'):
# Ignore 'require api X' lines in this pass. # Ignore 'require api X' lines in this pass.
@ -328,31 +344,28 @@ class DirectoryScan:
elif len(mline) != 3 or mline[1] != 'export': elif len(mline) != 3 or mline[1] != 'export':
# Currently we only support 'ba_meta export FOO'; # Currently we only support 'ba_meta export FOO';
# complain for anything else we see. # complain for anything else we see.
self.results.warnings += ( self.results.warnings.append(
'Warning: ' + str(subpath) + f'Warning: {subpath}'
': unrecognized ba_meta statement on line ' + f': unrecognized ba_meta statement on line {lindex + 1}.')
str(lindex + 1) + '.\n')
else: else:
# Looks like we've got a valid export line! # Looks like we've got a valid export line!
modulename = '.'.join(subpath.parts) modulename = '.'.join(subpath.parts)
if subpath.name.endswith('.py'): if subpath.name.endswith('.py'):
modulename = modulename[:-3] modulename = modulename[:-3]
exporttype = mline[2] exporttypestr = mline[2]
export_class_name = self._get_export_class_name( export_class_name = self._get_export_class_name(
subpath, flines, lindex) subpath, flines, lindex)
if export_class_name is not None: if export_class_name is not None:
classname = modulename + '.' + export_class_name classname = modulename + '.' + export_class_name
if exporttype == 'game':
self.results.games.append(classname) # If export type is one of our shortcuts, sub in the
elif exporttype == 'plugin': # actual class path. Otherwise assume its a classpath
self.results.plugins.append(classname) # itself.
elif exporttype == 'keyboard': exporttype = EXPORT_CLASS_NAME_SHORTCUTS.get(exporttypestr)
self.results.keyboards.append(classname) if exporttype is None:
else: exporttype = exporttypestr
self.results.warnings += ( self.results.exports.setdefault(exporttype,
'Warning: ' + str(subpath) + []).append(classname)
': unrecognized export type "' + exporttype +
'" on line ' + str(lindex + 1) + '.\n')
def _get_export_class_name(self, subpath: Path, lines: list[str], def _get_export_class_name(self, subpath: Path, lines: list[str],
lindex: int) -> str | None: lindex: int) -> str | None:
@ -374,13 +387,12 @@ class DirectoryScan:
classname = cbits[0] classname = cbits[0]
break # Success! break # Success!
if classname is None: if classname is None:
self.results.warnings += ( self.results.warnings.append(
'Warning: ' + str(subpath) + ': class definition not found' f'Warning: {subpath}: class definition not found below'
' below "ba_meta export" statement on line ' + f' "ba_meta export" statement on line {lindexorig + 1}.')
str(lindexorig + 1) + '.\n')
return classname return classname
def get_api_requirement( def _get_api_requirement(
self, self,
subpath: Path, subpath: Path,
meta_lines: dict[int, list[str]], meta_lines: dict[int, list[str]],
@ -401,15 +413,15 @@ class DirectoryScan:
# Ok; not successful. lets issue warnings for a few error cases. # Ok; not successful. lets issue warnings for a few error cases.
if len(lines) > 1: if len(lines) > 1:
self.results.warnings += ( self.results.warnings.append(
'Warning: ' + str(subpath) + f'Warning: {subpath}: multiple'
': multiple "# ba_meta require api <NUM>" lines found;' ' "# ba_meta require api <NUM>" lines found;'
' ignoring module.\n') ' ignoring module.')
elif not lines and toplevel and meta_lines: elif not lines and toplevel and meta_lines:
# If we're a top-level module containing meta lines but # If we're a top-level module containing meta lines but
# no valid "require api" line found, complain. # no valid "require api" line found, complain.
self.results.warnings += ( self.results.warnings.append(
'Warning: ' + str(subpath) + f'Warning: {subpath}:'
': no valid "# ba_meta require api <NUM>" line found;' ' no valid "# ba_meta require api <NUM>" line found;'
' ignoring module.\n') ' ignoring module.')
return None return None

View file

@ -94,9 +94,11 @@ class MultiTeamSession(Session):
playlist = _playlist.get_default_free_for_all_playlist() playlist = _playlist.get_default_free_for_all_playlist()
# Resolve types and whatnot to get our final playlist. # Resolve types and whatnot to get our final playlist.
playlist_resolved = _playlist.filter_playlist(playlist, playlist_resolved = _playlist.filter_playlist(
sessiontype=type(self), playlist,
add_resolved_type=True) sessiontype=type(self),
add_resolved_type=True,
name='default teams' if self.use_teams else 'default ffa')
if not playlist_resolved: if not playlist_resolved:
raise RuntimeError('Playlist contains no valid games.') raise RuntimeError('Playlist contains no valid games.')

View file

@ -134,15 +134,16 @@ class MasterServerCallThread(threading.Thread):
import json import json
from efro.error import is_urllib_communication_error 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 response_data: Any = None
url: str | None = None url: str | None = None
try: try:
self._data = _general.utf8_all(self._data) self._data = utf8_all(self._data)
_ba.set_thread_name('BA_ServerCallThread') _ba.set_thread_name('BA_ServerCallThread')
if self._request_type == 'get': 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)) '?' + urllib.parse.urlencode(self._data))
response = urllib.request.urlopen( response = urllib.request.urlopen(
urllib.request.Request( urllib.request.Request(
@ -150,7 +151,7 @@ class MasterServerCallThread(threading.Thread):
context=_ba.app.net.sslcontext, context=_ba.app.net.sslcontext,
timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS) timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS)
elif self._request_type == 'post': elif self._request_type == 'post':
url = _ba.get_master_server_address() + '/' + self._request url = get_master_server_address() + '/' + self._request
response = urllib.request.urlopen( response = urllib.request.urlopen(
urllib.request.Request( urllib.request.Request(
url, url,
@ -189,7 +190,7 @@ class MasterServerCallThread(threading.Thread):
response_data = None response_data = None
if self._callback is not 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) from_other_thread=True)

View file

@ -5,6 +5,7 @@
from __future__ import annotations from __future__ import annotations
import copy import copy
import logging
from typing import Any, TYPE_CHECKING from typing import Any, TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
@ -18,7 +19,8 @@ def filter_playlist(playlist: PlaylistType,
sessiontype: type[_session.Session], sessiontype: type[_session.Session],
add_resolved_type: bool = False, add_resolved_type: bool = False,
remove_unowned: bool = True, remove_unowned: bool = True,
mark_unowned: bool = False) -> PlaylistType: mark_unowned: bool = False,
name: str = '?') -> PlaylistType:
"""Return a filtered version of a playlist. """Return a filtered version of a playlist.
Strips out or replaces invalid or unowned game types, makes sure all 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-locals
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
# pylint: disable=too-many-statements # pylint: disable=too-many-statements
import _ba from ba._map import get_filtered_map_name
from ba import _map from ba._store import get_unowned_maps, get_unowned_game_types
from ba import _general from ba._general import getclass
from ba import _gameactivity from ba._gameactivity import GameActivity
goodlist: list[dict] = [] goodlist: list[dict] = []
unowned_maps: Sequence[str] unowned_maps: Sequence[str]
if remove_unowned or mark_unowned: if remove_unowned or mark_unowned:
unowned_maps = _map.get_unowned_maps() unowned_maps = get_unowned_maps()
unowned_game_types = _ba.app.meta.get_unowned_game_types() unowned_game_types = get_unowned_game_types()
else: else:
unowned_maps = [] unowned_maps = []
unowned_game_types = set() unowned_game_types = set()
@ -53,7 +55,7 @@ def filter_playlist(playlist: PlaylistType,
del entry['map'] del entry['map']
# Update old map names to new ones. # 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']) entry['settings']['map'])
if remove_unowned and entry['settings']['map'] in unowned_maps: if remove_unowned and entry['settings']['map'] in unowned_maps:
continue continue
@ -120,8 +122,7 @@ def filter_playlist(playlist: PlaylistType,
entry['type'] = ( entry['type'] = (
'bastd.game.targetpractice.TargetPracticeGame') 'bastd.game.targetpractice.TargetPracticeGame')
gameclass = _general.getclass(entry['type'], gameclass = getclass(entry['type'], GameActivity)
_gameactivity.GameActivity)
if remove_unowned and gameclass in unowned_game_types: if remove_unowned and gameclass in unowned_game_types:
continue continue
@ -139,7 +140,8 @@ def filter_playlist(playlist: PlaylistType,
entry['settings'][setting.name] = setting.default entry['settings'][setting.name] = setting.default
goodlist.append(entry) goodlist.append(entry)
except ImportError as exc: 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: except Exception:
from ba import _error from ba import _error
_error.print_exception() _error.print_exception()

View file

@ -4,6 +4,7 @@
from __future__ import annotations from __future__ import annotations
import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from dataclasses import dataclass from dataclasses import dataclass
@ -25,6 +26,46 @@ class PluginSubsystem:
self.potential_plugins: list[ba.PotentialPlugin] = [] self.potential_plugins: list[ba.PotentialPlugin] = []
self.active_plugins: dict[str, ba.Plugin] = {} 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: def on_app_running(self) -> None:
"""Should be called when the app reaches the running state.""" """Should be called when the app reaches the running state."""
# Load up our plugins and go ahead and call their on_app_running calls. # 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 from ba._language import Lstr
# Note: the plugins we load is purely based on what's enabled # 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 # in the app config. Its not our job to look at meta stuff here.
# plugins, but that is only used to give the user a list of plugins
# that they can enable. (we wouldn't want to look at meta-scan here
# anyway because it may not be done yet at this point in the launch)
plugstates: dict[str, dict] = _ba.app.config.get('Plugins', {}) plugstates: dict[str, dict] = _ba.app.config.get('Plugins', {})
assert isinstance(plugstates, dict) assert isinstance(plugstates, dict)
plugkeys: list[str] = sorted(key for key, val in plugstates.items() plugkeys: list[str] = sorted(key for key, val in plugstates.items()
@ -90,8 +128,7 @@ class PluginSubsystem:
subs=[('${PLUGIN}', plugkey), subs=[('${PLUGIN}', plugkey),
('${ERROR}', str(exc))]), ('${ERROR}', str(exc))]),
color=(1, 0, 0)) color=(1, 0, 0))
_ba.log(f"Error loading plugin class '{plugkey}': {exc}", logging.exception("Error loading plugin class '%s'", plugkey)
to_server=False)
continue continue
try: try:
plugin = cls() plugin = cls()
@ -118,10 +155,8 @@ class PluginSubsystem:
color=(1, 1, 0), color=(1, 1, 0),
) )
plugnames = ', '.join(disappeared_plugs) plugnames = ', '.join(disappeared_plugs)
_ba.log( logging.warning('%d plugin(s) no longer found: %s.',
f'{len(disappeared_plugs)} plugin(s) no longer found:' len(disappeared_plugs), plugnames)
f' {plugnames}.',
to_server=False)
for goneplug in disappeared_plugs: for goneplug in disappeared_plugs:
del _ba.app.config['Plugins'][goneplug] del _ba.app.config['Plugins'][goneplug]
_ba.app.config.commit() _ba.app.config.commit()

View file

@ -5,6 +5,7 @@ from __future__ import annotations
import sys import sys
import time import time
import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from efro.terminal import Clr from efro.terminal import Clr
@ -13,6 +14,8 @@ from bacommon.servermanager import (ServerCommand, StartServerModeCommand,
ChatMessageCommand, ScreenMessageCommand, ChatMessageCommand, ScreenMessageCommand,
ClientListCommand, KickCommand) ClientListCommand, KickCommand)
import _ba import _ba
from ba._internal import (add_transaction, run_transactions,
get_v1_account_state)
from ba._generated.enums import TimeType from ba._generated.enums import TimeType
from ba._freeforallsession import FreeForAllSession from ba._freeforallsession import FreeForAllSession
from ba._dualteamsession import DualTeamSession from ba._dualteamsession import DualTeamSession
@ -227,7 +230,7 @@ class ServerController:
def _prepare_to_serve(self) -> None: def _prepare_to_serve(self) -> None:
"""Run in a timer to do prep before beginning to serve.""" """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: if not signed_in:
# Signing in to the local server account should not take long; # Signing in to the local server account should not take long;
@ -247,14 +250,14 @@ class ServerController:
if not self._playlist_fetch_sent_request: if not self._playlist_fetch_sent_request:
print(f'{Clr.SBLU}Requesting shared-playlist' print(f'{Clr.SBLU}Requesting shared-playlist'
f' {self._config.playlist_code}...{Clr.RST}') f' {self._config.playlist_code}...{Clr.RST}')
_ba.add_transaction( add_transaction(
{ {
'type': 'IMPORT_PLAYLIST', 'type': 'IMPORT_PLAYLIST',
'code': str(self._config.playlist_code), 'code': str(self._config.playlist_code),
'overwrite': True 'overwrite': True
}, },
callback=self._on_playlist_fetch_response) callback=self._on_playlist_fetch_response)
_ba.run_transactions() run_transactions()
self._playlist_fetch_sent_request = True self._playlist_fetch_sent_request = True
if self._playlist_fetch_got_response: if self._playlist_fetch_got_response:
@ -302,7 +305,7 @@ class ServerController:
appcfg = app.config appcfg = app.config
sessiontype = self._get_session_type() 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 ' print('WARNING: launch_server_session() expects to run '
'with a signed in server account') 'with a signed in server account')
@ -322,21 +325,21 @@ class ServerController:
# Need to add this in a transaction instead of just setting # Need to add this in a transaction instead of just setting
# it directly or it will get overwritten by the master-server. # it directly or it will get overwritten by the master-server.
_ba.add_transaction({ add_transaction({
'type': 'ADD_PLAYLIST', 'type': 'ADD_PLAYLIST',
'playlistType': ptypename, 'playlistType': ptypename,
'playlistName': self._playlist_name, 'playlistName': self._playlist_name,
'playlist': self._config.playlist_inline 'playlist': self._config.playlist_inline
}) })
_ba.run_transactions() run_transactions()
if self._first_run: if self._first_run:
curtimestr = time.strftime('%c') curtimestr = time.strftime('%c')
_ba.log( startupmsg = (
f'{Clr.BLD}{Clr.BLU}{_ba.appnameupper()} {app.version}' f'{Clr.BLD}{Clr.BLU}{_ba.appnameupper()} {app.version}'
f' ({app.build_number})' f' ({app.build_number})'
f' entering server-mode {curtimestr}{Clr.RST}', f' entering server-mode {curtimestr}{Clr.RST}')
to_server=False) logging.info(startupmsg)
if sessiontype is FreeForAllSession: if sessiontype is FreeForAllSession:
appcfg['Free-for-All Playlist Selection'] = self._playlist_name appcfg['Free-for-All Playlist Selection'] = self._playlist_name

View file

@ -615,6 +615,7 @@ class Session:
def transitioning_out_activity_was_freed( def transitioning_out_activity_was_freed(
self, can_show_ad_on_death: bool) -> None: self, can_show_ad_on_death: bool) -> None:
"""(internal)""" """(internal)"""
# pylint: disable=cyclic-import
from ba._apputils import garbage_collect from ba._apputils import garbage_collect
# Since things should be generally still right now, it's a good time # 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 from typing import TYPE_CHECKING
import _ba import _ba
from ba import _internal
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any 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' '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.santa')
store_layout['characters'][0]['items'].append('characters.wizard') store_layout['characters'][0]['items'].append('characters.wizard')
store_layout['characters'][0]['items'].append('characters.cyborg') 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({ store_layout['characters'].append({
'title': 'store.holidaySpecialText', 'title': 'store.holidaySpecialText',
'items': ['characters.bunny'] '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: def get_available_purchase_count(tab: str | None = None) -> int:
"""(internal)""" """(internal)"""
try: try:
if _ba.get_v1_account_state() != 'signed_in': if _internal.get_v1_account_state() != 'signed_in':
return 0 return 0
count = 0 count = 0
our_tickets = _ba.get_v1_account_ticket_count() our_tickets = _internal.get_v1_account_ticket_count()
store_data = get_store_layout() store_data = get_store_layout()
if tab is not None: if tab is not None:
tabs = [(tab, store_data[tab])] 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: count: int) -> int:
for section in tabval: for section in tabval:
for item in section['items']: 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) 'price.' + item, None)
if ticket_cost is not None: if ticket_cost is not None:
if (our_tickets >= ticket_cost if (our_tickets >= ticket_cost
and not _ba.get_purchased(item)): and not _internal.get_purchased(item)):
count += 1 count += 1
return count 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 # We start the timer once we get the duration from
# the server. # the server.
start_duration = _ba.get_v1_account_misc_read_val( start_duration = _internal.get_v1_account_misc_read_val(
'proSaleDurationMinutes', None) 'proSaleDurationMinutes', None)
if start_duration is not None: if start_duration is not None:
app.pro_sale_start_time = int( app.pro_sale_start_time = int(
@ -489,12 +490,12 @@ def get_available_sale_time(tab: str) -> int | None:
sale_times.append(val) sale_times.append(val)
# Now look for sales in this tab. # 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() store_layout = get_store_layout()
for section in store_layout[tab]: for section in store_layout[tab]:
for item in section['items']: for item in section['items']:
if item in sales_raw: if item in sales_raw:
if not _ba.get_purchased(item): if not _internal.get_purchased(item):
to_end = ((datetime.datetime.utcfromtimestamp( to_end = ((datetime.datetime.utcfromtimestamp(
sales_raw[item]['e']) - sales_raw[item]['e']) -
datetime.datetime.utcnow()).total_seconds()) datetime.datetime.utcnow()).total_seconds())
@ -509,3 +510,35 @@ def get_available_sale_time(tab: str) -> int | None:
from ba import _error from ba import _error
_error.print_exception('error calcing sale time') _error.print_exception('error calcing sale time')
return None 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 or disappear without warning, so should be avoided (or used sparingly and
defensively) in mods. defensively) in mods.
""" """
from __future__ import annotations
from ba._map import (get_unowned_maps, get_map_class, register_map, from _ba import (
preload_map_preview_media, get_map_display_string, show_online_score_ui, set_ui_input_device, is_party_icon_visible,
get_filtered_map_name) 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._appconfig import commit_app_config
from ba._input import (get_device_value, get_input_map_hash, from ba._input import (get_device_value, get_input_map_hash,
get_input_device_config) 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, from ba._store import (get_available_sale_time, get_available_purchase_count,
get_store_item_name_translated, get_store_item_name_translated,
get_store_item_display_size, get_store_layout, 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._tournament import get_tournament_prize_strings
from ba._gameutils import get_trophy_string 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__ = [ __all__ = [
'get_unowned_maps', 'get_map_class', 'register_map', 'show_online_score_ui',
'preload_map_preview_media', 'get_map_display_string', 'set_ui_input_device',
'get_filtered_map_name', 'commit_app_config', 'get_device_value', 'is_party_icon_visible',
'get_input_map_hash', 'get_input_device_config', 'getclass', 'json_prep', 'getinputdevice',
'get_type_name', 'JoinActivity', 'ScoreScreenActivity', 'add_clean_frame_callback',
'is_browser_likely_available', 'get_remote_app_name', 'unlock_all_input',
'should_submit_debug_info', 'run_gpu_benchmark', 'run_cpu_benchmark', 'increment_analytics_count',
'run_media_reload_benchmark', 'run_stress_test', 'getcampaign', 'set_debug_speed_exponent',
'PlayerProfilesChangedMessage', 'DEFAULT_TEAM_COLORS', 'get_special_widget',
'DEFAULT_TEAM_NAMES', 'do_play_music', 'master_server_get', 'get_qrcode_texture',
'master_server_post', 'get_ip_address_type', 'get_string_height',
'DEFAULT_REQUEST_TIMEOUT_SECONDS', 'get_default_powerup_distribution', 'get_string_width',
'get_player_profile_colors', 'get_player_profile_icon', 'show_app_invite',
'get_player_colors', 'get_next_tip', 'get_default_free_for_all_playlist', 'appnameupper',
'get_default_teams_playlist', 'filter_playlist', 'get_available_sale_time', 'lock_all_input',
'get_available_purchase_count', 'get_store_item_name_translated', 'open_file_externally',
'get_store_item_display_size', 'get_store_layout', 'get_store_item', 'fade_screen',
'get_clean_price', 'get_tournament_prize_strings', 'get_trophy_string' '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 # 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. # if we need to change behavior server-side to go along with client changes.
BACLOUD_VERSION = 7 BACLOUD_VERSION = 8
@ioprepped @ioprepped

View file

@ -21,7 +21,7 @@ class LoginProxyRequestMessage(Message):
"""Request send to the cloud to ask for a login-proxy.""" """Request send to the cloud to ask for a login-proxy."""
@classmethod @classmethod
def get_response_types(cls) -> list[type[Response]]: def get_response_types(cls) -> list[type[Response] | None]:
return [LoginProxyRequestResponse] return [LoginProxyRequestResponse]
@ -48,7 +48,7 @@ class LoginProxyStateQueryMessage(Message):
proxykey: Annotated[str, IOAttrs('k')] proxykey: Annotated[str, IOAttrs('k')]
@classmethod @classmethod
def get_response_types(cls) -> list[type[Response]]: def get_response_types(cls) -> list[type[Response] | None]:
return [LoginProxyStateQueryResponse] return [LoginProxyStateQueryResponse]
@ -82,7 +82,7 @@ class PingMessage(Message):
"""Standard ping.""" """Standard ping."""
@classmethod @classmethod
def get_response_types(cls) -> list[type[Response]]: def get_response_types(cls) -> list[type[Response] | None]:
return [PingResponse] return [PingResponse]
@ -99,7 +99,7 @@ class TestMessage(Message):
testfoo: Annotated[int, IOAttrs('f')] testfoo: Annotated[int, IOAttrs('f')]
@classmethod @classmethod
def get_response_types(cls) -> list[type[Response]]: def get_response_types(cls) -> list[type[Response] | None]:
return [TestResponse] return [TestResponse]
@ -130,7 +130,7 @@ class WorkspaceFetchMessage(Message):
state: Annotated[WorkspaceFetchState, IOAttrs('s')] state: Annotated[WorkspaceFetchState, IOAttrs('s')]
@classmethod @classmethod
def get_response_types(cls) -> list[type[Response]]: def get_response_types(cls) -> list[type[Response] | None]:
return [WorkspaceFetchResponse] return [WorkspaceFetchResponse]

View file

@ -6,12 +6,11 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import _ba
import ba import ba
from ba.internal import JoinActivity from ba.internal import JoinActivity
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any, Sequence pass
class CoopJoinActivity(JoinActivity): class CoopJoinActivity(JoinActivity):
@ -25,16 +24,6 @@ class CoopJoinActivity(JoinActivity):
session = self.session session = self.session
assert isinstance(session, ba.CoopSession) 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: def on_transition_in(self) -> None:
from bastd.actor.controlsguide import ControlsGuide from bastd.actor.controlsguide import ControlsGuide
from bastd.actor.text import Text from bastd.actor.text import Text
@ -53,143 +42,61 @@ class CoopJoinActivity(JoinActivity):
position=(0, -95)).autoretain() position=(0, -95)).autoretain()
ControlsGuide(delay=1.0).autoretain() ControlsGuide(delay=1.0).autoretain()
def _on_got_scores_to_beat(self, ba.pushcall(self._show_remaining_achievements)
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
# Sort by originating date so that the most recent is first. def _show_remaining_achievements(self) -> None:
if scores is not None: from bastd.actor.text import Text
scores.sort(reverse=True,
key=lambda score: asserttype(score['time'], int))
# We only show achievements and challenges for CoopGameActivities. # We only show achievements and challenges for CoopGameActivities.
session = self.session session = self.session
assert isinstance(session, ba.CoopSession) assert isinstance(session, ba.CoopSession)
gameinstance = session.get_current_game_instance() gameinstance = session.get_current_game_instance()
if isinstance(gameinstance, ba.CoopGameActivity): if not isinstance(gameinstance, ba.CoopGameActivity):
score_type = gameinstance.get_score_type() return
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 = []
delay = 1.0 delay = 1.0
vpos = -140.0 vpos = -140.0
spacing = 25
delay_inc = 0.1
def _add_t( # Now list our remaining achievements for this level.
text: str | ba.Lstr, assert self.session.campaign is not None
h_offs: float = 0.0, assert isinstance(self.session, ba.CoopSession)
scale: float = 1.0, levelname = (self.session.campaign.name + ':' +
color: Sequence[float] = (1.0, 1.0, 1.0, 0.46) self.session.campaign_level_name)
) -> None: ts_h_offs = 60
Text(text,
scale=scale * 0.76, if not (ba.app.demo_mode or ba.app.arcade_mode):
h_align=Text.HAlign.LEFT, achievements = [
a for a in ba.app.ach.achievements_for_coop_level(levelname)
if not a.complete
]
have_achievements = bool(achievements)
achievements = [a for a in achievements if not a.complete]
vrmode = ba.app.vr_mode
if have_achievements:
Text(ba.Lstr(resource='achievementsRemainingText'),
host_only=True,
position=(ts_h_offs - 10, vpos),
transition=Text.Transition.FADE_IN,
scale=1.1 * 0.76,
h_attach=Text.HAttach.LEFT, h_attach=Text.HAttach.LEFT,
v_attach=Text.VAttach.TOP, v_attach=Text.VAttach.TOP,
transition=Text.Transition.FADE_IN, color=(1, 1, 1.2, 1) if vrmode else (0.8, 0.8, 1, 1),
transition_delay=delay, shadow=1.0,
color=color, flatness=1.0 if vrmode else 0.6,
position=(60 + h_offs, vpos)).autoretain() transition_delay=delay).autoretain()
hval = ts_h_offs + 50
if score_challenges: vpos -= 35
_add_t(ba.Lstr(value='${A}:', for ach in achievements:
subs=[('${A}', delay += 0.05
ba.Lstr(resource='scoreChallengesText')) ach.create_display(hval, vpos, delay, style='in_game')
]), vpos -= 55
scale=1.1) if not achievements:
delay += delay_inc Text(ba.Lstr(resource='noAchievementsRemainingText'),
vpos -= spacing
for chal in score_challenges:
_add_t(str(chal['value'] if score_type == 'points' else ba.
timestring(int(chal['value']) * 10,
timeformat=ba.TimeFormat.MILLISECONDS
).evaluate()) + ' (1 player)',
h_offs=30,
color=(0.9, 0.7, 1.0, 0.8))
delay += delay_inc
vpos -= 0.6 * spacing
_add_t(chal['player'],
h_offs=40,
color=(0.8, 1, 0.8, 0.6),
scale=0.8)
delay += delay_inc
vpos -= 1.2 * spacing
vpos -= 0.5 * spacing
if achievement_challenges:
_add_t(ba.Lstr(
value='${A}:',
subs=[('${A}',
ba.Lstr(resource='achievementChallengesText'))]),
scale=1.1)
delay += delay_inc
vpos -= spacing
for chal in achievement_challenges:
_add_t(str(chal['value']),
h_offs=30,
color=(0.9, 0.7, 1.0, 0.8))
delay += delay_inc
vpos -= 0.6 * spacing
_add_t(chal['player'],
h_offs=40,
color=(0.8, 1, 0.8, 0.6),
scale=0.8)
delay += delay_inc
vpos -= 1.2 * spacing
vpos -= 0.5 * spacing
# Now list our remaining achievements for this level.
assert self.session.campaign is not None
assert isinstance(self.session, ba.CoopSession)
levelname = (self.session.campaign.name + ':' +
self.session.campaign_level_name)
ts_h_offs = 60
if not (ba.app.demo_mode or ba.app.arcade_mode):
achievements = [
a
for a in ba.app.ach.achievements_for_coop_level(levelname)
if not a.complete
]
have_achievements = bool(achievements)
achievements = [a for a in achievements if not a.complete]
vrmode = ba.app.vr_mode
if have_achievements:
Text(ba.Lstr(resource='achievementsRemainingText'),
host_only=True, host_only=True,
position=(ts_h_offs - 10, vpos), position=(ts_h_offs + 15, vpos + 10),
transition=Text.Transition.FADE_IN, transition=Text.Transition.FADE_IN,
scale=1.1 * 0.76, scale=0.7,
h_attach=Text.HAttach.LEFT, h_attach=Text.HAttach.LEFT,
v_attach=Text.VAttach.TOP, v_attach=Text.VAttach.TOP,
color=(1, 1, 1.2, 1) if vrmode else (0.8, 0.8, 1, 1), color=(1, 1, 1, 0.5),
shadow=1.0, transition_delay=delay + 0.5).autoretain()
flatness=1.0 if vrmode else 0.6,
transition_delay=delay).autoretain()
hval = ts_h_offs + 50
vpos -= 35
for ach in achievements:
delay += 0.05
ach.create_display(hval, vpos, delay, style='in_game')
vpos -= 55
if not achievements:
Text(ba.Lstr(resource='noAchievementsRemainingText'),
host_only=True,
position=(ts_h_offs + 15, vpos + 10),
transition=Text.Transition.FADE_IN,
scale=0.7,
h_attach=Text.HAttach.LEFT,
v_attach=Text.VAttach.TOP,
color=(1, 1, 1, 0.5),
transition_delay=delay + 0.5).autoretain()

View file

@ -8,8 +8,8 @@ from __future__ import annotations
import random import random
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import _ba
import ba import ba
import ba.internal
from bastd.actor.text import Text from bastd.actor.text import Text
from bastd.actor.zoomtext import ZoomText 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 + ':' + ba.app.ach.achievements_for_coop_level(self._campaign.name + ':' +
settings['level'])) settings['level']))
self._account_type = (_ba.get_v1_account_type() self._account_type = (ba.internal.get_v1_account_type()
if _ba.get_v1_account_state() == 'signed_in' else if ba.internal.get_v1_account_state()
None) == 'signed_in' else None)
self._game_service_icon_color: Sequence[float] | None self._game_service_icon_color: Sequence[float] | None
self._game_service_achievements_texture: ba.Texture | 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' # If game-center/etc scores are available we show our friends'
# scores. Otherwise we show our local high scores. # 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) self._game_name_str, self._game_config_str)
try: try:
@ -264,12 +264,12 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
self.end({'outcome': 'next_level'}) self.end({'outcome': 'next_level'})
def _ui_gc(self) -> None: def _ui_gc(self) -> None:
_ba.show_online_score_ui('leaderboard', ba.internal.show_online_score_ui('leaderboard',
game=self._game_name_str, game=self._game_name_str,
game_version=self._game_config_str) game_version=self._game_config_str)
def _ui_show_achievements(self) -> None: 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: def _ui_worlds_best(self) -> None:
if self._score_link is 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). # to the game (like on mac).
can_select_extra_buttons = ba.app.platform == 'android' 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: if self._show_friend_scores:
ba.buttonwidget(parent=rootc, ba.buttonwidget(parent=rootc,
@ -483,7 +483,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
timetype=ba.TimeType.REAL) timetype=ba.TimeType.REAL)
def _update_corner_button_positions(self) -> None: 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 assert self._corner_button_offs is not None
pos_x = self._corner_button_offs[0] + offs pos_x = self._corner_button_offs[0] + offs
pos_y = self._corner_button_offs[1] 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 # 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. # 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): 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) assert isinstance(self._server_transitioning, bool)
# If server-mode is handling this, don't do anything ourself. # 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: if ba.app.server is not None:
# Host can't press retry button, so anyone can do it instead. # Host can't press retry button, so anyone can do it instead.
time_till_assign = max( 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)) 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. # Any time we complete a level, set the next one as unlocked.
if self._is_complete and self._is_more_levels: if self._is_complete and self._is_more_levels:
_ba.add_transaction({ ba.internal.add_transaction({
'type': 'COMPLETE_LEVEL', 'type': 'COMPLETE_LEVEL',
'campaign': self._campaign.name, 'campaign': self._campaign.name,
'level': self._level_name 'level': self._level_name
@ -632,7 +632,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
if ba.app.server is None: if ba.app.server is None:
# If we're running in normal non-headless build, show this text # If we're running in normal non-headless build, show this text
# because only host can continue the game. # 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', txt = Text(ba.Lstr(resource='waitingForHostText',
subs=[('${HOST}', adisp)]), subs=[('${HOST}', adisp)]),
maxwidth=300, maxwidth=300,
@ -726,14 +726,14 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
if self._score is not None: if self._score is not None:
sver = (self._campaign.getlevel( sver = (self._campaign.getlevel(
self._level_name).get_score_version_string()) self._level_name).get_score_version_string())
_ba.add_transaction({ ba.internal.add_transaction({
'type': 'SET_LEVEL_LOCAL_HIGH_SCORES', 'type': 'SET_LEVEL_LOCAL_HIGH_SCORES',
'campaign': self._campaign.name, 'campaign': self._campaign.name,
'level': self._level_name, 'level': self._level_name,
'scoreVersion': sver, 'scoreVersion': sver,
'scores': our_high_scores_all '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. # We expect this only in kiosk mode; complain otherwise.
if not (ba.app.demo_mode or ba.app.arcade_mode): if not (ba.app.demo_mode or ba.app.arcade_mode):
print('got not-signed-in at score-submit; unexpected') print('got not-signed-in at score-submit; unexpected')
@ -743,21 +743,22 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
else: else:
assert self._game_name_str is not None assert self._game_name_str is not None
assert self._game_config_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_config_str, self._game_name_str,
name_str, self._game_config_str,
self._score, name_str,
ba.WeakCall(self._got_score_results), self._score,
ba.WeakCall(self._got_friend_score_results) ba.WeakCall(self._got_score_results),
if self._show_friend_scores else None, ba.WeakCall(self._got_friend_score_results)
order=self._score_order, if self._show_friend_scores else None,
tournament_id=self.session.tournament_id, order=self._score_order,
score_type=self._score_type, tournament_id=self.session.tournament_id,
campaign=self._campaign.name, score_type=self._score_type,
level=self._level_name) campaign=self._campaign.name,
level=self._level_name)
# Apply the transactions we've been adding locally. # 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 # If we're not doing the world's-best button, just show a title
# instead. # instead.
@ -1074,9 +1075,12 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
else: else:
self._score_link = results['link'] self._score_link = results['link']
assert self._score_link is not None assert self._score_link is not None
if not self._score_link.startswith('http://'): # Prepend our master-server addr if its a relative addr.
self._score_link = (_ba.get_master_server_address() + '/' + if (not self._score_link.startswith('http://')
self._score_link) and not self._score_link.startswith('https://')):
self._score_link = (
ba.internal.get_master_server_address() + '/' +
self._score_link)
self._score_loading_status = None self._score_loading_status = None
if 'tournamentSecondsRemaining' in results: if 'tournamentSecondsRemaining' in results:
secs_remaining = results['tournamentSecondsRemaining'] secs_remaining = results['tournamentSecondsRemaining']

View file

@ -6,8 +6,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import _ba
import ba import ba
import ba.internal
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any, Sequence from typing import Any, Sequence
@ -317,9 +317,8 @@ class ControlsGuide(ba.Actor):
# an input device that is *not* the touchscreen. # an input device that is *not* the touchscreen.
# (otherwise it is confusing to see the touchscreen buttons right # (otherwise it is confusing to see the touchscreen buttons right
# next to our display buttons) # next to our display buttons)
touchscreen: ba.InputDevice | None = _ba.getinputdevice('TouchScreen', touchscreen: ba.InputDevice | None = ba.internal.getinputdevice(
'#1', 'TouchScreen', '#1', doraise=False)
doraise=False)
if touchscreen is not None: if touchscreen is not None:
# We look at the session's players; not the activity's. # 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 # If there's no players with input devices yet, try to default to
# showing keyboard controls. # showing keyboard controls.
if not input_devices: if not input_devices:
kbd = _ba.getinputdevice('Keyboard', '#1', doraise=False) kbd = ba.internal.getinputdevice('Keyboard', '#1', doraise=False)
if kbd is not None: if kbd is not None:
input_devices.append(kbd) input_devices.append(kbd)

View file

@ -36,9 +36,9 @@ class PopupText(ba.Actor):
if len(color) == 3: if len(color) == 3:
color = (color[0], color[1], color[2], 1.0) color = (color[0], color[1], color[2], 1.0)
pos = (position[0] + offset[0] + random_offset * 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] + 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', self.node = ba.newnode('text',
attrs={ attrs={

View file

@ -81,7 +81,7 @@ class Spaz(ba.Actor):
factory = SpazFactory.get() 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._demo_mode = demo_mode
self.play_big_death_sound = False self.play_big_death_sound = False
@ -758,7 +758,7 @@ class Spaz(ba.Actor):
tex = PowerupBoxFactory.get().tex_punch tex = PowerupBoxFactory.get().tex_punch
self._flash_billboard(tex) self._flash_billboard(tex)
self.equip_boxing_gloves() 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.boxing_gloves_flashing = False
self.node.mini_billboard_3_texture = tex self.node.mini_billboard_3_texture = tex
t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) t_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS)
@ -966,7 +966,7 @@ class Spaz(ba.Actor):
self.on_punched(damage) self.on_punched(damage)
# If damage was significant, lets show it. # If damage was significant, lets show it.
if damage > 350: if damage >= 350:
assert msg.force_direction is not None assert msg.force_direction is not None
ba.show_damage_count('-' + str(int(damage / 10)) + '%', ba.show_damage_count('-' + str(int(damage / 10)) + '%',
msg.pos, msg.force_direction) msg.pos, msg.force_direction)
@ -977,11 +977,13 @@ class Spaz(ba.Actor):
ba.playsound(SpazFactory.get().punch_sound_stronger, ba.playsound(SpazFactory.get().punch_sound_stronger,
1.0, 1.0,
position=self.node.position) position=self.node.position)
if damage > 500: if damage >= 500:
sounds = SpazFactory.get().punch_sound_strong sounds = SpazFactory.get().punch_sound_strong
sound = sounds[random.randrange(len(sounds))] sound = sounds[random.randrange(len(sounds))]
else: elif damage >= 100:
sound = SpazFactory.get().punch_sound sound = SpazFactory.get().punch_sound
else:
sound = SpazFactory.get().punch_sound_weak
ba.playsound(sound, 1.0, position=self.node.position) ba.playsound(sound, 1.0, position=self.node.position)
# Throw up some chunks. # Throw up some chunks.
@ -1075,7 +1077,7 @@ class Spaz(ba.Actor):
# us if its grown high enough. # us if its grown high enough.
if self.hitpoints <= 0: if self.hitpoints <= 0:
damage_avg = self.node.damage_smoothed * damage_scale damage_avg = self.node.damage_smoothed * damage_scale
if damage_avg > 1000: if damage_avg >= 1000:
self.shatter() self.shatter()
elif isinstance(msg, BombDiedMessage): elif isinstance(msg, BombDiedMessage):
@ -1341,9 +1343,9 @@ class Spaz(ba.Actor):
hit_type='impact')) hit_type='impact'))
self.node.handlemessage('knockout', max(0.0, 50.0 * intensity)) self.node.handlemessage('knockout', max(0.0, 50.0 * intensity))
sounds: Sequence[ba.Sound] sounds: Sequence[ba.Sound]
if intensity > 5.0: if intensity >= 5.0:
sounds = SpazFactory.get().impact_sounds_harder sounds = SpazFactory.get().impact_sounds_harder
elif intensity > 3.0: elif intensity >= 3.0:
sounds = SpazFactory.get().impact_sounds_hard sounds = SpazFactory.get().impact_sounds_hard
else: else:
sounds = SpazFactory.get().impact_sounds_medium sounds = SpazFactory.get().impact_sounds_medium

View file

@ -5,8 +5,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import _ba
import ba import ba
import ba.internal
if TYPE_CHECKING: if TYPE_CHECKING:
pass pass
@ -16,66 +16,67 @@ def get_appearances(include_locked: bool = False) -> list[str]:
"""Get the list of available spaz appearances.""" """Get the list of available spaz appearances."""
# pylint: disable=too-many-statements # pylint: disable=too-many-statements
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
get_purchased = ba.internal.get_purchased
disallowed = [] disallowed = []
if not include_locked: if not include_locked:
# hmm yeah this'll be tough to hack... # 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') disallowed.append('Santa Claus')
if not _ba.get_purchased('characters.frosty'): if not get_purchased('characters.frosty'):
disallowed.append('Frosty') disallowed.append('Frosty')
if not _ba.get_purchased('characters.bones'): if not get_purchased('characters.bones'):
disallowed.append('Bones') disallowed.append('Bones')
if not _ba.get_purchased('characters.bernard'): if not get_purchased('characters.bernard'):
disallowed.append('Bernard') disallowed.append('Bernard')
if not _ba.get_purchased('characters.pixie'): if not get_purchased('characters.pixie'):
disallowed.append('Pixel') disallowed.append('Pixel')
if not _ba.get_purchased('characters.pascal'): if not get_purchased('characters.pascal'):
disallowed.append('Pascal') disallowed.append('Pascal')
if not _ba.get_purchased('characters.actionhero'): if not get_purchased('characters.actionhero'):
disallowed.append('Todd McBurton') disallowed.append('Todd McBurton')
if not _ba.get_purchased('characters.taobaomascot'): if not get_purchased('characters.taobaomascot'):
disallowed.append('Taobao Mascot') disallowed.append('Taobao Mascot')
if not _ba.get_purchased('characters.agent'): if not get_purchased('characters.agent'):
disallowed.append('Agent Johnson') disallowed.append('Agent Johnson')
if not _ba.get_purchased('characters.jumpsuit'): if not get_purchased('characters.jumpsuit'):
disallowed.append('Lee') disallowed.append('Lee')
if not _ba.get_purchased('characters.assassin'): if not get_purchased('characters.assassin'):
disallowed.append('Zola') disallowed.append('Zola')
if not _ba.get_purchased('characters.wizard'): if not get_purchased('characters.wizard'):
disallowed.append('Grumbledorf') disallowed.append('Grumbledorf')
if not _ba.get_purchased('characters.cowboy'): if not get_purchased('characters.cowboy'):
disallowed.append('Butch') disallowed.append('Butch')
if not _ba.get_purchased('characters.witch'): if not get_purchased('characters.witch'):
disallowed.append('Witch') disallowed.append('Witch')
if not _ba.get_purchased('characters.warrior'): if not get_purchased('characters.warrior'):
disallowed.append('Warrior') disallowed.append('Warrior')
if not _ba.get_purchased('characters.superhero'): if not get_purchased('characters.superhero'):
disallowed.append('Middle-Man') disallowed.append('Middle-Man')
if not _ba.get_purchased('characters.alien'): if not get_purchased('characters.alien'):
disallowed.append('Alien') disallowed.append('Alien')
if not _ba.get_purchased('characters.oldlady'): if not get_purchased('characters.oldlady'):
disallowed.append('OldLady') disallowed.append('OldLady')
if not _ba.get_purchased('characters.gladiator'): if not get_purchased('characters.gladiator'):
disallowed.append('Gladiator') disallowed.append('Gladiator')
if not _ba.get_purchased('characters.wrestler'): if not get_purchased('characters.wrestler'):
disallowed.append('Wrestler') disallowed.append('Wrestler')
if not _ba.get_purchased('characters.operasinger'): if not get_purchased('characters.operasinger'):
disallowed.append('Gretel') disallowed.append('Gretel')
if not _ba.get_purchased('characters.robot'): if not get_purchased('characters.robot'):
disallowed.append('Robot') disallowed.append('Robot')
if not _ba.get_purchased('characters.cyborg'): if not get_purchased('characters.cyborg'):
disallowed.append('B-9000') disallowed.append('B-9000')
if not _ba.get_purchased('characters.bunny'): if not get_purchased('characters.bunny'):
disallowed.append('Easter Bunny') disallowed.append('Easter Bunny')
if not _ba.get_purchased('characters.kronk'): if not get_purchased('characters.kronk'):
disallowed.append('Kronk') disallowed.append('Kronk')
if not _ba.get_purchased('characters.zoe'): if not get_purchased('characters.zoe'):
disallowed.append('Zoe') disallowed.append('Zoe')
if not _ba.get_purchased('characters.jackmorgan'): if not get_purchased('characters.jackmorgan'):
disallowed.append('Jack Morgan') disallowed.append('Jack Morgan')
if not _ba.get_purchased('characters.mel'): if not get_purchased('characters.mel'):
disallowed.append('Mel') disallowed.append('Mel')
if not _ba.get_purchased('characters.snakeshadow'): if not get_purchased('characters.snakeshadow'):
disallowed.append('Snake Shadow') disallowed.append('Snake Shadow')
return [ return [
s for s in list(ba.app.spaz_appearances.keys()) if s not in disallowed 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 from typing import TYPE_CHECKING
import ba import ba
import ba.internal
from bastd.gameutils import SharedObjects from bastd.gameutils import SharedObjects
import _ba
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any, Sequence from typing import Any, Sequence
@ -38,6 +38,9 @@ class SpazFactory:
"""The sound that plays for an 'important' spaz death such as in """The sound that plays for an 'important' spaz death such as in
co-op games.""" co-op games."""
punch_sound_weak: ba.Sound
"""A weak punch ba.Sound."""
punch_sound: ba.Sound punch_sound: ba.Sound
"""A standard punch ba.Sound.""" """A standard punch ba.Sound."""
@ -98,6 +101,7 @@ class SpazFactory:
self.impact_sounds_harder = (ba.getsound('bigImpact'), self.impact_sounds_harder = (ba.getsound('bigImpact'),
ba.getsound('bigImpact2')) ba.getsound('bigImpact2'))
self.single_player_death_sound = ba.getsound('playerDeath') self.single_player_death_sound = ba.getsound('playerDeath')
self.punch_sound_weak = ba.getsound('punchWeak01')
self.punch_sound = ba.getsound('punch01') self.punch_sound = ba.getsound('punch01')
self.punch_sound_strong = (ba.getsound('punchStrong01'), self.punch_sound_strong = (ba.getsound('punchStrong01'),
ba.getsound('punchStrong02')) ba.getsound('punchStrong02'))
@ -208,15 +212,18 @@ class SpazFactory:
# Lets load some basic rules. # Lets load some basic rules.
# (allows them to be tweaked from the master server) # (allows them to be tweaked from the master server)
self.shield_decay_rate = _ba.get_v1_account_misc_read_val('rsdr', 10.0) self.shield_decay_rate = ba.internal.get_v1_account_misc_read_val(
self.punch_cooldown = _ba.get_v1_account_misc_read_val('rpc', 400) 'rsdr', 10.0)
self.punch_cooldown_gloves = (_ba.get_v1_account_misc_read_val( 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)) 'rpcg', 300))
self.punch_power_scale = _ba.get_v1_account_misc_read_val('rpp', 1.2) self.punch_power_scale = ba.internal.get_v1_account_misc_read_val(
self.punch_power_scale_gloves = (_ba.get_v1_account_misc_read_val( 'rpp', 1.2)
'rppg', 1.4)) self.punch_power_scale_gloves = (
self.max_shield_spillover_damage = (_ba.get_v1_account_misc_read_val( ba.internal.get_v1_account_misc_read_val('rppg', 1.4))
'rsms', 500)) self.max_shield_spillover_damage = (
ba.internal.get_v1_account_misc_read_val('rsms', 500))
def get_style(self, character: str) -> str: def get_style(self, character: str) -> str:
"""Return the named style for this character. """Return the named style for this character.

View file

@ -44,7 +44,10 @@ class EasterEggHuntGame(ba.TeamGameActivity[Player, Team]):
name = 'Easter Egg Hunt' name = 'Easter Egg Hunt'
description = 'Gather eggs!' 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) scoreconfig = ba.ScoreConfig(label='Score', scoretype=ba.ScoreType.POINTS)
# We're currently hard-coded for one map. # 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.egg_tex_3 = ba.gettexture('eggTex3')
self._collect_sound = ba.getsound('powerup01') self._collect_sound = ba.getsound('powerup01')
self._pro_mode = settings.get('Pro Mode', False) self._pro_mode = settings.get('Pro Mode', False)
self._epic_mode = settings.get('Epic Mode', False)
self._max_eggs = 1.0 self._max_eggs = 1.0
self.egg_material = ba.Material() self.egg_material = ba.Material()
self.egg_material.add_actions( self.egg_material.add_actions(
@ -81,7 +85,9 @@ class EasterEggHuntGame(ba.TeamGameActivity[Player, Team]):
self._bots: SpazBotSet | None = None self._bots: SpazBotSet | None = None
# Base class overrides # 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: def on_team_join(self, team: Team) -> None:
if self.has_begun(): if self.has_begun():

View file

@ -106,8 +106,8 @@ class FootballTeamGame(ba.TeamGameActivity[Player, Team]):
], ],
default=1.0, default=1.0,
), ),
ba.BoolSetting('Epic Mode', default=False),
] ]
default_music = ba.MusicType.FOOTBALL
@classmethod @classmethod
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: 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._flag_respawn_light: ba.NodeActor | None = None
self._score_to_win = int(settings['Score to Win']) self._score_to_win = int(settings['Score to Win'])
self._time_limit = float(settings['Time Limit']) 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: def get_instance_description(self) -> str | Sequence:
touchdowns = self._score_to_win / 7 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} >'] tips = ['Use the pick-up button to grab the flag < ${PICKUP} >']
scoreconfig = ba.ScoreConfig(scoretype=ba.ScoreType.MILLISECONDS, scoreconfig = ba.ScoreConfig(scoretype=ba.ScoreType.MILLISECONDS,
version='B') version='B')
default_music = ba.MusicType.FOOTBALL default_music = ba.MusicType.FOOTBALL
# FIXME: Need to update co-op games to use getscoreconfig. # 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, default=1.0,
), ),
ba.BoolSetting('Epic Mode', default=False),
] ]
default_music = ba.MusicType.HOCKEY
@classmethod @classmethod
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: 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._puck: Puck | None = None
self._score_to_win = int(settings['Score to Win']) self._score_to_win = int(settings['Score to Win'])
self._time_limit = float(settings['Time Limit']) 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: def get_instance_description(self) -> str | Sequence:
if self._score_to_win == 1: if self._score_to_win == 1:

View file

@ -76,9 +76,9 @@ class KeepAwayGame(ba.TeamGameActivity[Player, Team]):
], ],
default=1.0, default=1.0,
), ),
ba.BoolSetting('Epic Mode', default=False),
] ]
scoreconfig = ba.ScoreConfig(label='Time Held') scoreconfig = ba.ScoreConfig(label='Time Held')
default_music = ba.MusicType.KEEP_AWAY
@classmethod @classmethod
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: 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._flag: Flag | None = None
self._hold_time = int(settings['Hold Time']) self._hold_time = int(settings['Hold Time'])
self._time_limit = float(settings['Time Limit']) 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: def get_instance_description(self) -> str | Sequence:
return 'Carry the flag for ${ARG1} seconds.', self._hold_time 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, default=1.0,
), ),
ba.BoolSetting('Epic Mode', default=False),
] ]
scoreconfig = ba.ScoreConfig(label='Time Held') 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._scoring_team: weakref.ref[Team] | None = None
self._hold_time = int(settings['Hold Time']) self._hold_time = int(settings['Hold Time'])
self._time_limit = float(settings['Time Limit']) self._time_limit = float(settings['Time Limit'])
self._epic_mode = bool(settings['Epic Mode'])
self._flag_region_material = ba.Material() self._flag_region_material = ba.Material()
self._flag_region_material.add_actions( self._flag_region_material.add_actions(
conditions=('they_have_material', shared.player_material), conditions=('they_have_material', shared.player_material),
@ -128,7 +130,9 @@ class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]):
)) ))
# Base class overrides. # 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: def get_instance_description(self) -> str | Sequence:
return 'Secure the flag for ${ARG1} seconds.', self._hold_time return 'Secure the flag for ${ARG1} seconds.', self._hold_time

View file

@ -10,7 +10,7 @@ import weakref
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import ba import ba
import _ba import ba.internal
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any 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 # host is navigating menus while they're just staring at an
# empty-ish screen. # empty-ish screen.
tval = ba.Lstr(resource='hostIsNavigatingMenusText', 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( self._host_is_navigating_text = ba.NodeActor(
ba.newnode('text', ba.newnode('text',
attrs={ attrs={
@ -251,7 +252,7 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]):
self._update() self._update()
# Hopefully this won't hitch but lets space these out anyway. # 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() 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. # We now want to wait until we're signed in before fetching news.
def _try_fetching_news(self) -> None: 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_news()
self._fetch_timer = None 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() ba.app.main_menu_last_news_fetch_time = time.time()
# UPDATE - We now just pull news from MRVs. # 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: if news is not None:
self._got_news(news) self._got_news(news)
@ -453,6 +454,11 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]):
ba.app.ui.set_main_menu_window( ba.app.ui.set_main_menu_window(
CoopBrowserWindow( CoopBrowserWindow(
transition=None).get_root_widget()) 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: else:
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
from bastd.ui.mainmenu import MainMenuWindow 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: 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 'logoEaster'
return None return None
@ -930,7 +936,7 @@ class MainMenuSession(ba.Session):
def on_activity_end(self, activity: ba.Activity, results: Any) -> None: def on_activity_end(self, activity: ba.Activity, results: Any) -> None:
if self._locked: if self._locked:
_ba.unlock_all_input() ba.internal.unlock_all_input()
# Any ending activity leads us into the main menu one. # Any ending activity leads us into the main menu one.
self.setactivity(ba.newactivity(MainMenuActivity)) self.setactivity(ba.newactivity(MainMenuActivity))

View file

@ -18,8 +18,8 @@ from __future__ import annotations
import math import math
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import _ba
import ba import ba
import ba.internal
from bastd.actor import spaz as basespaz from bastd.actor import spaz as basespaz
if TYPE_CHECKING: if TYPE_CHECKING:
@ -235,7 +235,7 @@ class TutorialActivity(ba.Activity[Player, Team]):
super().on_begin() super().on_begin()
ba.set_analytics_screen('Tutorial Start') ba.set_analytics_screen('Tutorial Start')
_ba.increment_analytics_count('Tutorial start') ba.internal.increment_analytics_count('Tutorial start')
if bool(False): if bool(False):
# Buttons on top. # Buttons on top.
@ -461,7 +461,7 @@ class TutorialActivity(ba.Activity[Player, Team]):
def run(self, a: TutorialActivity) -> None: def run(self, a: TutorialActivity) -> None:
print('setting to', self._speed) print('setting to', self._speed)
_ba.set_debug_speed_exponent(self._speed) ba.internal.set_debug_speed_exponent(self._speed)
class RemoveGloves: class RemoveGloves:
@ -609,7 +609,7 @@ class TutorialActivity(ba.Activity[Player, Team]):
pass pass
def run(self, a: TutorialActivity) -> None: def run(self, a: TutorialActivity) -> None:
_ba.increment_analytics_count('Tutorial finish') ba.internal.increment_analytics_count('Tutorial finish')
a.end() a.end()
class Move: class Move:
@ -2328,7 +2328,7 @@ class TutorialActivity(ba.Activity[Player, Team]):
('${TOTAL}', str(len(self.players)))]) if count > 0 else '' ('${TOTAL}', str(len(self.players)))]) if count > 0 else ''
if (count >= len(self.players) and self.players if (count >= len(self.players) and self.players
and not self._have_skipped): and not self._have_skipped):
_ba.increment_analytics_count('Tutorial skip') ba.internal.increment_analytics_count('Tutorial skip')
ba.set_analytics_screen('Tutorial Skip') ba.set_analytics_screen('Tutorial Skip')
self._have_skipped = True self._have_skipped = True
ba.playsound(ba.getsound('swish')) ba.playsound(ba.getsound('swish'))

View file

@ -4,7 +4,6 @@
from __future__ import annotations from __future__ import annotations
import _ba
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.""" """Bring up a prompt telling the user they must sign in."""
from bastd.ui.confirm import ConfirmWindow from bastd.ui.confirm import ConfirmWindow
from bastd.ui.account import settings from bastd.ui.account import settings
from ba.internal import sign_in_v1
if account_type == 'Google Play': if account_type == 'Google Play':
ConfirmWindow( ConfirmWindow(
ba.Lstr(resource='notSignedInGooglePlayErrorText'), ba.Lstr(resource='notSignedInGooglePlayErrorText'),
lambda: _ba.sign_in_v1('Google Play'), lambda: sign_in_v1('Google Play'),
ok_text=ba.Lstr(resource='accountSettingsWindow.signInText'), ok_text=ba.Lstr(resource='accountSettingsWindow.signInText'),
width=460, width=460,
height=130) height=130)

View file

@ -8,8 +8,8 @@ import copy
import time import time
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import _ba
import ba import ba
import ba.internal
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any from typing import Any
@ -50,7 +50,8 @@ class AccountLinkWindow(ba.Window):
autoselect=True, autoselect=True,
icon=ba.gettexture('crossOut'), icon=ba.gettexture('crossOut'),
iconscale=1.2) 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( ba.textwidget(
parent=self._root_widget, parent=self._root_widget,
position=(self._width * 0.5, self._height * 0.56), position=(self._width * 0.5, self._height * 0.56),
@ -84,17 +85,17 @@ class AccountLinkWindow(ba.Window):
def _generate_press(self) -> None: def _generate_press(self) -> None:
from bastd.ui import account 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() account.show_sign_in_prompt()
return return
ba.screenmessage( ba.screenmessage(
ba.Lstr(resource='gatherWindow.requestingAPromoCodeText'), ba.Lstr(resource='gatherWindow.requestingAPromoCodeText'),
color=(0, 1, 0)) color=(0, 1, 0))
_ba.add_transaction({ ba.internal.add_transaction({
'type': 'ACCOUNT_LINK_CODE_REQUEST', 'type': 'ACCOUNT_LINK_CODE_REQUEST',
'expire_time': time.time() + 5 'expire_time': time.time() + 5
}) })
_ba.run_transactions() ba.internal.run_transactions()
def _enter_code_press(self) -> None: def _enter_code_press(self) -> None:
from bastd.ui import promocode from bastd.ui import promocode

View file

@ -8,8 +8,8 @@ from __future__ import annotations
import time import time
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import _ba
import ba import ba
import ba.internal
if TYPE_CHECKING: if TYPE_CHECKING:
pass pass
@ -25,7 +25,6 @@ class AccountSettingsWindow(ba.Window):
close_once_signed_in: bool = False): close_once_signed_in: bool = False):
# pylint: disable=too-many-statements # 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_v2_button: ba.Widget | None = None
self._sign_in_device_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._r = 'accountSettingsWindow'
self._modal = modal self._modal = modal
self._needs_refresh = False self._needs_refresh = False
self._signed_in = (_ba.get_v1_account_state() == 'signed_in') self._signed_in = (ba.internal.get_v1_account_state() == 'signed_in')
self._account_state_num = _ba.get_v1_account_state_num() self._account_state_num = ba.internal.get_v1_account_state_num()
self._show_linked = (self._signed_in 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)) 'allowAccountLinking2', False))
self._check_sign_in_timer = ba.Timer(1.0, self._check_sign_in_timer = ba.Timer(1.0,
ba.WeakCall(self._update), ba.WeakCall(self._update),
@ -58,7 +57,7 @@ class AccountSettingsWindow(ba.Window):
# Currently we can only reset achievements on game-center. # Currently we can only reset achievements on game-center.
account_type: str | None account_type: str | None
if self._signed_in: if self._signed_in:
account_type = _ba.get_v1_account_type() account_type = ba.internal.get_v1_account_type()
else: else:
account_type = None account_type = None
self._can_reset_achievements = (account_type == 'Game Center') self._can_reset_achievements = (account_type == 'Game Center')
@ -84,9 +83,6 @@ class AccountSettingsWindow(ba.Window):
if app.platform == 'android' and app.subplatform == 'google': if app.platform == 'android' and app.subplatform == 'google':
self._show_sign_in_buttons.append('Google Play') 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 # Local accounts are generally always available with a few key
# exceptions. # exceptions.
self._show_sign_in_buttons.append('Local') self._show_sign_in_buttons.append('Local')
@ -159,11 +155,12 @@ class AccountSettingsWindow(ba.Window):
# Hmm should update this to use get_account_state_num. # Hmm should update this to use get_account_state_num.
# Theoretically if we switch from one signed-in account to another # Theoretically if we switch from one signed-in account to another
# in the background this would break. # in the background this would break.
account_state_num = _ba.get_v1_account_state_num() account_state_num = ba.internal.get_v1_account_state_num()
account_state = _ba.get_v1_account_state() 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
'allowAccountLinking2', False)) and ba.internal.get_v1_account_misc_read_val(
'allowAccountLinking2', False))
if (account_state_num != self._account_state_num if (account_state_num != self._account_state_num
or self._show_linked != show_linked or self._needs_refresh): or self._show_linked != show_linked or self._needs_refresh):
@ -191,8 +188,8 @@ class AccountSettingsWindow(ba.Window):
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
from bastd.ui import confirm from bastd.ui import confirm
account_state = _ba.get_v1_account_state() account_state = ba.internal.get_v1_account_state()
account_type = (_ba.get_v1_account_type() account_type = (ba.internal.get_v1_account_type()
if account_state == 'signed_in' else 'unknown') if account_state == 'signed_in' else 'unknown')
is_google = account_type == 'Google Play' is_google = account_type == 'Google Play'
@ -212,27 +209,24 @@ class AccountSettingsWindow(ba.Window):
show_google_play_sign_in_button = (account_state == 'signed_out' show_google_play_sign_in_button = (account_state == 'signed_out'
and 'Google Play' and 'Google Play'
in self._show_sign_in_buttons) 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' show_device_sign_in_button = (account_state == 'signed_out' and 'Local'
in self._show_sign_in_buttons) in self._show_sign_in_buttons)
show_v2_sign_in_button = (account_state == 'signed_out' show_v2_sign_in_button = (account_state == 'signed_out'
and 'V2' in self._show_sign_in_buttons) and 'V2' in self._show_sign_in_buttons)
sign_in_button_space = 70.0 sign_in_button_space = 70.0
show_game_service_button = (self._signed_in and account_type show_game_service_button = (self._signed_in
in ['Game Center', 'Game Circle']) and account_type in ['Game Center'])
game_service_button_space = 60.0 game_service_button_space = 60.0
show_linked_accounts_text = (self._signed_in show_linked_accounts_text = (self._signed_in and
and _ba.get_v1_account_misc_read_val( ba.internal.get_v1_account_misc_read_val(
'allowAccountLinking2', False)) 'allowAccountLinking2', False))
linked_accounts_text_space = 60.0 linked_accounts_text_space = 60.0
show_achievements_button = ( show_achievements_button = (self._signed_in and account_type
self._signed_in in ('Google Play', 'Alibaba', 'Local',
and account_type in ('Google Play', 'Alibaba', 'Local', 'OUYA')) 'OUYA', 'V2'))
achievements_button_space = 60.0 achievements_button_space = 60.0
show_achievements_text = (self._signed_in show_achievements_text = (self._signed_in
@ -251,11 +245,17 @@ class AccountSettingsWindow(ba.Window):
show_reset_progress_button = False show_reset_progress_button = False
reset_progress_button_space = 70.0 reset_progress_button_space = 70.0
show_player_profiles_button = self._signed_in show_manage_v2_account_button = (self._signed_in
player_profiles_button_space = 100.0 and account_type == 'V2'
and bool(False)) # Disabled for now.
manage_v2_account_button_space = 100.0
show_link_accounts_button = (self._signed_in show_player_profiles_button = self._signed_in
and _ba.get_v1_account_misc_read_val( 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)) 'allowAccountLinking2', False))
link_accounts_button_space = 70.0 link_accounts_button_space = 70.0
@ -282,8 +282,6 @@ class AccountSettingsWindow(ba.Window):
self._sub_height += signing_in_text_space self._sub_height += signing_in_text_space
if show_google_play_sign_in_button: if show_google_play_sign_in_button:
self._sub_height += sign_in_button_space 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: if show_device_sign_in_button:
self._sub_height += sign_in_button_space self._sub_height += sign_in_button_space
if show_v2_sign_in_button: if show_v2_sign_in_button:
@ -306,6 +304,8 @@ class AccountSettingsWindow(ba.Window):
self._sub_height += sign_in_benefits_space self._sub_height += sign_in_benefits_space
if show_reset_progress_button: if show_reset_progress_button:
self._sub_height += reset_progress_button_space 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: if show_player_profiles_button:
self._sub_height += player_profiles_button_space self._sub_height += player_profiles_button_space
if show_link_accounts_button: if show_link_accounts_button:
@ -335,7 +335,8 @@ class AccountSettingsWindow(ba.Window):
size=(0, 0), size=(0, 0),
text=ba.Lstr( text=ba.Lstr(
resource='accountSettingsWindow.deviceSpecificAccountText', resource='accountSettingsWindow.deviceSpecificAccountText',
subs=[('${NAME}', _ba.get_v1_account_display_string())]), subs=[('${NAME}',
ba.internal.get_v1_account_display_string())]),
scale=0.7, scale=0.7,
color=(0.5, 0.5, 0.6), color=(0.5, 0.5, 0.6),
maxwidth=self._sub_width * 0.9, maxwidth=self._sub_width * 0.9,
@ -376,7 +377,7 @@ class AccountSettingsWindow(ba.Window):
self._account_name_text = None self._account_name_text = None
if self._back_button is None: if self._back_button is None:
bbtn = _ba.get_special_widget('back_button') bbtn = ba.internal.get_special_widget('back_button')
else: else:
bbtn = self._back_button bbtn = self._back_button
@ -444,32 +445,8 @@ class AccountSettingsWindow(ba.Window):
first_selectable = btn first_selectable = btn
if ba.app.ui.use_toolbars: if ba.app.ui.use_toolbars:
ba.widget(edit=btn, ba.widget(edit=btn,
right_widget=_ba.get_special_widget('party_button')) right_widget=ba.internal.get_special_widget(
ba.widget(edit=btn, left_widget=bbtn) 'party_button'))
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'))
ba.widget(edit=btn, left_widget=bbtn) ba.widget(edit=btn, left_widget=bbtn)
ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
self._sign_in_text = None self._sign_in_text = None
@ -514,7 +491,8 @@ class AccountSettingsWindow(ba.Window):
first_selectable = btn first_selectable = btn
if ba.app.ui.use_toolbars: if ba.app.ui.use_toolbars:
ba.widget(edit=btn, 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, left_widget=bbtn)
ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
self._sign_in_text = None self._sign_in_text = None
@ -560,11 +538,34 @@ class AccountSettingsWindow(ba.Window):
first_selectable = btn first_selectable = btn
if ba.app.ui.use_toolbars: if ba.app.ui.use_toolbars:
ba.widget(edit=btn, 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, left_widget=bbtn)
ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
self._sign_in_text = None 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: if show_player_profiles_button:
button_width = 300 button_width = 300
v -= player_profiles_button_space v -= player_profiles_button_space
@ -582,18 +583,17 @@ class AccountSettingsWindow(ba.Window):
first_selectable = btn first_selectable = btn
if ba.app.ui.use_toolbars: if ba.app.ui.use_toolbars:
ba.widget(edit=btn, 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) ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=0)
# the button to go to OS-Specific leaderboards/high-score-lists/etc. # the button to go to OS-Specific leaderboards/high-score-lists/etc.
if show_game_service_button: if show_game_service_button:
button_width = 300 button_width = 300
v -= game_service_button_space * 0.85 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': if account_type == 'Game Center':
account_type_name = ba.Lstr(resource='gameCenterText') account_type_name = ba.Lstr(resource='gameCenterText')
elif account_type == 'Game Circle':
account_type_name = ba.Lstr(resource='gameCircleText')
else: else:
raise ValueError("unknown account type: '" + raise ValueError("unknown account type: '" +
str(account_type) + "'") str(account_type) + "'")
@ -603,14 +603,15 @@ class AccountSettingsWindow(ba.Window):
color=(0.55, 0.5, 0.6), color=(0.55, 0.5, 0.6),
textcolor=(0.75, 0.7, 0.8), textcolor=(0.75, 0.7, 0.8),
autoselect=True, autoselect=True,
on_activate_call=_ba.show_online_score_ui, on_activate_call=ba.internal.show_online_score_ui,
size=(button_width, 50), size=(button_width, 50),
label=account_type_name) label=account_type_name)
if first_selectable is None: if first_selectable is None:
first_selectable = btn first_selectable = btn
if ba.app.ui.use_toolbars: if ba.app.ui.use_toolbars:
ba.widget(edit=btn, 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, left_widget=bbtn)
v -= game_service_button_space * 0.15 v -= game_service_button_space * 0.15
else: else:
@ -652,7 +653,8 @@ class AccountSettingsWindow(ba.Window):
first_selectable = btn first_selectable = btn
if ba.app.ui.use_toolbars: if ba.app.ui.use_toolbars:
ba.widget(edit=btn, 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, left_widget=bbtn)
v -= achievements_button_space * 0.15 v -= achievements_button_space * 0.15
else: else:
@ -680,7 +682,8 @@ class AccountSettingsWindow(ba.Window):
first_selectable = btn first_selectable = btn
if ba.app.ui.use_toolbars: if ba.app.ui.use_toolbars:
ba.widget(edit=btn, 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, left_widget=bbtn)
v -= leaderboards_button_space * 0.15 v -= leaderboards_button_space * 0.15
else: else:
@ -750,7 +753,8 @@ class AccountSettingsWindow(ba.Window):
first_selectable = btn first_selectable = btn
if ba.app.ui.use_toolbars: if ba.app.ui.use_toolbars:
ba.widget(edit=btn, 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, left_widget=bbtn)
self._linked_accounts_text: ba.Widget | None self._linked_accounts_text: ba.Widget | None
@ -805,7 +809,8 @@ class AccountSettingsWindow(ba.Window):
first_selectable = btn first_selectable = btn
if ba.app.ui.use_toolbars: if ba.app.ui.use_toolbars:
ba.widget(edit=btn, 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) ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50)
self._unlink_accounts_button: ba.Widget | None self._unlink_accounts_button: ba.Widget | None
@ -833,7 +838,8 @@ class AccountSettingsWindow(ba.Window):
first_selectable = btn first_selectable = btn
if ba.app.ui.use_toolbars: if ba.app.ui.use_toolbars:
ba.widget(edit=btn, 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) ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50)
self._update_unlink_accounts_button() self._update_unlink_accounts_button()
else: else:
@ -854,7 +860,8 @@ class AccountSettingsWindow(ba.Window):
first_selectable = btn first_selectable = btn
if ba.app.ui.use_toolbars: if ba.app.ui.use_toolbars:
ba.widget(edit=btn, 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) ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15)
if show_cancel_v2_sign_in_button: if show_cancel_v2_sign_in_button:
@ -872,7 +879,8 @@ class AccountSettingsWindow(ba.Window):
first_selectable = btn first_selectable = btn
if ba.app.ui.use_toolbars: if ba.app.ui.use_toolbars:
ba.widget(edit=btn, 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) ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15)
# Whatever the topmost selectable thing is, we want it to scroll all # 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: def _on_achievements_press(self) -> None:
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
from bastd.ui import achievements from bastd.ui import achievements
account_state = _ba.get_v1_account_state() account_state = ba.internal.get_v1_account_state()
account_type = (_ba.get_v1_account_type() account_type = (ba.internal.get_v1_account_type()
if account_state == 'signed_in' else 'unknown') if account_state == 'signed_in' else 'unknown')
# for google play we use the built-in UI; otherwise pop up our own # for google play we use the built-in UI; otherwise pop up our own
if account_type == 'Google Play': if account_type == 'Google Play':
ba.timer(0.15, 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) timetype=ba.TimeType.REAL)
elif account_type != 'unknown': elif account_type != 'unknown':
assert self._achievements_button is not None assert self._achievements_button is not None
@ -907,15 +915,16 @@ class AccountSettingsWindow(ba.Window):
def _on_leaderboards_press(self) -> None: def _on_leaderboards_press(self) -> None:
ba.timer(0.15, 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) timetype=ba.TimeType.REAL)
def _have_unlinkable_accounts(self) -> bool: def _have_unlinkable_accounts(self) -> bool:
# if this is not present, we haven't had contact from the server so # if this is not present, we haven't had contact from the server so
# let's not proceed.. # let's not proceed..
if _ba.get_public_login_id() is None: if ba.internal.get_public_login_id() is None:
return False 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 return len(accounts) > 1
def _update_unlink_accounts_button(self) -> None: 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 # if this is not present, we haven't had contact from the server so
# let's not proceed.. # 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 num = int(time.time()) % 4
accounts_str = num * '.' + (4 - num) * ' ' accounts_str = num * '.' + (4 - num) * ' '
else: 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() # our_account = _bs.get_v1_account_display_string()
# accounts = [a for a in accounts if a != our_account] # accounts = [a for a in accounts if a != our_account]
# accounts_str = u', '.join(accounts) if accounts else # accounts_str = u', '.join(accounts) if accounts else
@ -977,7 +987,7 @@ class AccountSettingsWindow(ba.Window):
if self._tickets_text is None: if self._tickets_text is None:
return return
try: try:
tc_str = str(_ba.get_v1_account_ticket_count()) tc_str = str(ba.internal.get_v1_account_ticket_count())
except Exception: except Exception:
ba.print_exception() ba.print_exception()
tc_str = '-' tc_str = '-'
@ -989,7 +999,7 @@ class AccountSettingsWindow(ba.Window):
if self._account_name_text is None: if self._account_name_text is None:
return return
try: try:
name_str = _ba.get_v1_account_display_string() name_str = ba.internal.get_v1_account_display_string()
except Exception: except Exception:
ba.print_exception() ba.print_exception()
name_str = '??' name_str = '??'
@ -1043,7 +1053,7 @@ class AccountSettingsWindow(ba.Window):
if ba.app.accounts_v2.have_primary_credentials(): if ba.app.accounts_v2.have_primary_credentials():
ba.app.accounts_v2.set_primary_credentials(None) ba.app.accounts_v2.set_primary_credentials(None)
else: else:
_ba.sign_out_v1() ba.internal.sign_out_v1()
cfg = ba.app.config cfg = ba.app.config
@ -1061,7 +1071,7 @@ class AccountSettingsWindow(ba.Window):
account_type: str, account_type: str,
show_test_warning: bool = True) -> None: show_test_warning: bool = True) -> None:
del show_test_warning # unused 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. # Make note of the type account we're *wanting* to be signed in with.
cfg = ba.app.config cfg = ba.app.config
@ -1082,7 +1092,7 @@ class AccountSettingsWindow(ba.Window):
# FIXME: This would need to happen server-side these days. # FIXME: This would need to happen server-side these days.
if self._can_reset_achievements: if self._can_reset_achievements:
ba.app.config['Achievements'] = {} ba.app.config['Achievements'] = {}
_ba.reset_achievements() ba.internal.reset_achievements()
campaign = getcampaign('Default') campaign = getcampaign('Default')
campaign.reset() # also writes the config.. campaign.reset() # also writes the config..
campaign = getcampaign('Challenges') campaign = getcampaign('Challenges')

View file

@ -7,8 +7,8 @@ from __future__ import annotations
import time import time
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import _ba
import ba import ba
import ba.internal
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any from typing import Any
@ -77,11 +77,11 @@ class AccountUnlinkWindow(ba.Window):
margin=0, margin=0,
left_border=10) 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: if our_login_id is None:
entries = [] entries = []
else: else:
account_infos = _ba.get_v1_account_misc_read_val_2( account_infos = ba.internal.get_v1_account_misc_read_val_2(
'linkedAccounts2', []) 'linkedAccounts2', [])
entries = [{ entries = [{
'name': ai['d'], 'name': ai['d'],
@ -108,12 +108,12 @@ class AccountUnlinkWindow(ba.Window):
ba.screenmessage(ba.Lstr(resource='pleaseWaitText', ba.screenmessage(ba.Lstr(resource='pleaseWaitText',
fallback_resource='requestingText'), fallback_resource='requestingText'),
color=(0, 1, 0)) color=(0, 1, 0))
_ba.add_transaction({ ba.internal.add_transaction({
'type': 'ACCOUNT_UNLINK_REQUEST', 'type': 'ACCOUNT_UNLINK_REQUEST',
'accountID': entry['id'], 'accountID': entry['id'],
'expire_time': time.time() + 5 'expire_time': time.time() + 5
}) })
_ba.run_transactions() ba.internal.run_transactions()
ba.containerwidget(edit=self._root_widget, ba.containerwidget(edit=self._root_widget,
transition=self._transition_out) transition=self._transition_out)

View file

@ -8,7 +8,7 @@ import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import ba import ba
import _ba import ba.internal
from efro.error import CommunicationError from efro.error import CommunicationError
import bacommon.cloud import bacommon.cloud
@ -81,7 +81,8 @@ class V2SignInWindow(ba.Window):
return return
# Show link(s) the user can use to log in. # 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://') address_pretty = address.removeprefix('https://')
ba.textwidget( ba.textwidget(
@ -123,7 +124,7 @@ class V2SignInWindow(ba.Window):
position=(self._width * 0.5 - qr_size * 0.5, position=(self._width * 0.5 - qr_size * 0.5,
self._height * 0.36 + qroffs - qr_size * 0.5), self._height * 0.36 + qroffs - qr_size * 0.5),
size=(qr_size, qr_size), size=(qr_size, qr_size),
texture=_ba.get_qrcode_texture(address)) texture=ba.internal.get_qrcode_texture(address))
# Start querying for results. # Start querying for results.
self._proxyid = response.proxyid self._proxyid = response.proxyid

View file

@ -6,8 +6,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import _ba
import ba import ba
import ba.internal
from bastd.ui import popup from bastd.ui import popup
if TYPE_CHECKING: if TYPE_CHECKING:
@ -91,8 +91,9 @@ class AccountViewerWindow(popup.PopupWindow):
# In cases where the user most likely has a browser/email, lets # In cases where the user most likely has a browser/email, lets
# offer a 'report this user' button. # offer a 'report this user' button.
if (is_browser_likely_available() and _ba.get_v1_account_misc_read_val( if (is_browser_likely_available()
'showAccountExtrasMenu', False)): and ba.internal.get_v1_account_misc_read_val(
'showAccountExtrasMenu', False)):
self._extras_menu_button = ba.buttonwidget( self._extras_menu_button = ba.buttonwidget(
parent=self.root_widget, parent=self.root_widget,
@ -154,11 +155,11 @@ class AccountViewerWindow(popup.PopupWindow):
delegate=self) delegate=self)
def _on_ban_press(self) -> None: def _on_ban_press(self) -> None:
_ba.add_transaction({ ba.internal.add_transaction({
'type': 'BAN_ACCOUNT', 'type': 'BAN_ACCOUNT',
'account': self._account_id 'account': self._account_id
}) })
_ba.run_transactions() ba.internal.run_transactions()
def _on_report_press(self) -> None: def _on_report_press(self) -> None:
from bastd.ui import report from bastd.ui import report
@ -166,8 +167,8 @@ class AccountViewerWindow(popup.PopupWindow):
origin_widget=self._extras_menu_button) origin_widget=self._extras_menu_button)
def _on_more_press(self) -> None: def _on_more_press(self) -> None:
ba.open_url(_ba.get_master_server_address() + '/highscores?profile=' + ba.open_url(ba.internal.get_master_server_address() +
self._account_id) '/highscores?profile=' + self._account_id)
def _on_query_response(self, data: dict[str, Any] | None) -> None: def _on_query_response(self, data: dict[str, Any] | None) -> None:
# FIXME: Tidy this up. # FIXME: Tidy this up.
@ -197,8 +198,8 @@ class AccountViewerWindow(popup.PopupWindow):
ba.print_exception('Error displaying trophies.') ba.print_exception('Error displaying trophies.')
account_name_spacing = 15 account_name_spacing = 15
tscale = 0.65 tscale = 0.65
ts_height = _ba.get_string_height(trophystr, ts_height = ba.internal.get_string_height(
suppress_warning=True) trophystr, suppress_warning=True)
sub_width = self._width - 80 sub_width = self._width - 80
sub_height = 200 + ts_height * tscale + \ sub_height = 200 + ts_height * tscale + \
account_name_spacing * len(data['accountDisplayStrings']) account_name_spacing * len(data['accountDisplayStrings'])
@ -321,8 +322,8 @@ class AccountViewerWindow(popup.PopupWindow):
('${SUFFIX}', '')]).evaluate() ('${SUFFIX}', '')]).evaluate()
rank_str_width = min( rank_str_width = min(
sub_width * maxwidth_scale, sub_width * maxwidth_scale,
_ba.get_string_width(rank_str, suppress_warning=True) * ba.internal.get_string_width(
0.55) rank_str, suppress_warning=True) * 0.55)
# Only tack our suffix on if its at the end and only for # Only tack our suffix on if its at the end and only for
# non-diamond leagues. # non-diamond leagues.
@ -374,8 +375,8 @@ class AccountViewerWindow(popup.PopupWindow):
]).evaluate() ]).evaluate()
rank_str_width = min( rank_str_width = min(
sub_width * maxwidth_scale, sub_width * maxwidth_scale,
_ba.get_string_width(rank_str, suppress_warning=True) * ba.internal.get_string_width(
0.3) rank_str, suppress_warning=True) * 0.3)
# Only tack our suffix on if its at the end and only for # Only tack our suffix on if its at the end and only for
# non-diamond leagues. # non-diamond leagues.

View file

@ -8,8 +8,8 @@ import copy
import time import time
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import _ba
import ba import ba
import ba.internal
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any from typing import Any
@ -62,11 +62,11 @@ class AppInviteWindow(ba.Window):
'gatherWindow.earnTicketsForRecommendingText'), 'gatherWindow.earnTicketsForRecommendingText'),
subs=[('${COUNT}', subs=[('${COUNT}',
str( str(
_ba.get_v1_account_misc_read_val( ba.internal.get_v1_account_misc_read_val(
'friendTryTickets', 300))), 'friendTryTickets', 300))),
('${YOU_COUNT}', ('${YOU_COUNT}',
str( str(
_ba.get_v1_account_misc_read_val( ba.internal.get_v1_account_misc_read_val(
'friendTryAwardTickets', 100)))])) 'friendTryAwardTickets', 100)))]))
or_text = ba.Lstr(resource='orText', or_text = ba.Lstr(resource='orText',
@ -104,14 +104,14 @@ class AppInviteWindow(ba.Window):
on_activate_call=ba.WeakCall(self._send_code)) on_activate_call=ba.WeakCall(self._send_code))
# kick off a transaction to get our code # kick off a transaction to get our code
_ba.add_transaction( ba.internal.add_transaction(
{ {
'type': 'FRIEND_PROMO_CODE_REQUEST', 'type': 'FRIEND_PROMO_CODE_REQUEST',
'ali': False, 'ali': False,
'expire_time': time.time() + 20 'expire_time': time.time() + 20
}, },
callback=ba.WeakCall(self._on_code_result)) 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: def _on_code_result(self, result: dict[str, Any] | None) -> None:
if result is not None: if result is not None:
@ -128,18 +128,18 @@ class AppInviteWindow(ba.Window):
ba.playsound(ba.getsound('error')) ba.playsound(ba.getsound('error'))
return 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.set_analytics_screen('App Invite UI')
_ba.show_app_invite( ba.internal.show_app_invite(
ba.Lstr(resource='gatherWindow.appInviteTitleText', ba.Lstr(resource='gatherWindow.appInviteTitleText',
subs=[('${APP_NAME}', ba.Lstr(resource='titleText')) subs=[('${APP_NAME}', ba.Lstr(resource='titleText'))
]).evaluate(), ]).evaluate(),
ba.Lstr(resource='gatherWindow.appInviteMessageText', ba.Lstr(resource='gatherWindow.appInviteMessageText',
subs=[ subs=[('${COUNT}', str(self._data['tickets'])),
('${COUNT}', str(self._data['tickets'])), ('${NAME}',
('${NAME}', _ba.get_v1_account_name().split()[0]), ba.internal.get_v1_account_name().split()[0]),
('${APP_NAME}', ba.Lstr(resource='titleText')) ('${APP_NAME}', ba.Lstr(resource='titleText'))
]).evaluate(), self._data['code']) ]).evaluate(), self._data['code'])
else: else:
ba.playsound(ba.getsound('error')) ba.playsound(ba.getsound('error'))
@ -250,13 +250,14 @@ class ShowFriendCodeWindow(ba.Window):
def _google_invites(self) -> None: def _google_invites(self) -> None:
ba.set_analytics_screen('App Invite UI') ba.set_analytics_screen('App Invite UI')
_ba.show_app_invite( ba.internal.show_app_invite(
ba.Lstr(resource='gatherWindow.appInviteTitleText', ba.Lstr(resource='gatherWindow.appInviteTitleText',
subs=[('${APP_NAME}', ba.Lstr(resource='titleText')) subs=[('${APP_NAME}', ba.Lstr(resource='titleText'))
]).evaluate(), ]).evaluate(),
ba.Lstr(resource='gatherWindow.appInviteMessageText', ba.Lstr(resource='gatherWindow.appInviteMessageText',
subs=[('${COUNT}', str(self._data['tickets'])), 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')) ('${APP_NAME}', ba.Lstr(resource='titleText'))
]).evaluate(), self._data['code']) ]).evaluate(), self._data['code'])
@ -264,7 +265,7 @@ class ShowFriendCodeWindow(ba.Window):
import urllib.parse import urllib.parse
# If somehow we got signed out. # 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'), ba.screenmessage(ba.Lstr(resource='notSignedInText'),
color=(1, 0, 0)) color=(1, 0, 0))
ba.playsound(ba.getsound('error')) ba.playsound(ba.getsound('error'))
@ -273,7 +274,7 @@ class ShowFriendCodeWindow(ba.Window):
ba.set_analytics_screen('Email Friend Code') ba.set_analytics_screen('Email Friend Code')
subject = (ba.Lstr(resource='gatherWindow.friendHasSentPromoCodeText'). subject = (ba.Lstr(resource='gatherWindow.friendHasSentPromoCodeText').
evaluate().replace( evaluate().replace(
'${NAME}', _ba.get_v1_account_name()).replace( '${NAME}', ba.internal.get_v1_account_name()).replace(
'${APP_NAME}', '${APP_NAME}',
ba.Lstr(resource='titleText').evaluate()).replace( ba.Lstr(resource='titleText').evaluate()).replace(
'${COUNT}', str(self._data['tickets']))) '${COUNT}', str(self._data['tickets'])))
@ -304,7 +305,7 @@ def handle_app_invites_press(force_code: bool = False) -> None:
"""(internal)""" """(internal)"""
app = ba.app app = ba.app
do_app_invites = (app.platform == 'android' and app.subplatform == 'google' 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) 'enableAppInvites', False) and not app.on_tv)
if force_code: if force_code:
do_app_invites = False do_app_invites = False
@ -326,11 +327,11 @@ def handle_app_invites_press(force_code: bool = False) -> None:
else: else:
ShowFriendCodeWindow(result) ShowFriendCodeWindow(result)
_ba.add_transaction( ba.internal.add_transaction(
{ {
'type': 'FRIEND_PROMO_CODE_REQUEST', 'type': 'FRIEND_PROMO_CODE_REQUEST',
'ali': False, 'ali': False,
'expire_time': time.time() + 10 'expire_time': time.time() + 10
}, },
callback=handle_result) callback=handle_result)
_ba.run_transactions() ba.internal.run_transactions()

View file

@ -7,8 +7,8 @@ from __future__ import annotations
import math import math
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import _ba
import ba import ba
import ba.internal
from bastd.ui import popup from bastd.ui import popup
if TYPE_CHECKING: if TYPE_CHECKING:
@ -156,7 +156,7 @@ class CharacterPicker(popup.PopupWindow):
def _on_store_press(self) -> None: def _on_store_press(self) -> None:
from bastd.ui.account import show_sign_in_prompt from bastd.ui.account import show_sign_in_prompt
from bastd.ui.store.browser import StoreBrowserWindow 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() show_sign_in_prompt()
return return
self._transition_out() self._transition_out()

View file

@ -6,8 +6,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import _ba
import ba import ba
import ba.internal
if TYPE_CHECKING: if TYPE_CHECKING:
pass pass
@ -29,7 +29,7 @@ class ConfigErrorWindow(ba.Window):
h_align='center', h_align='center',
v_align='top', v_align='top',
scale=0.73, 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' ':\n\n\nCheck the console'
' (press ~ twice) for details.\n\nWould you like to quit and' ' (press ~ twice) for details.\n\nWould you like to quit and'
' try to fix it by hand\nor overwrite it with defaults?\n\n' ' 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: def _quit(self) -> None:
ba.timer(0.001, self._edit_and_quit, timetype=ba.TimeType.REAL) 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: 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) ba.timer(0.1, ba.quit, timetype=ba.TimeType.REAL)
def _defaults(self) -> None: def _defaults(self) -> None:

View file

@ -6,8 +6,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import _ba
import ba import ba
import ba.internal
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any, Callable from typing import Any, Callable
@ -54,7 +54,7 @@ class ConfirmWindow:
size=(width, height), size=(width, height),
transition=transition, transition=transition,
toolbar_visibility='menu_minimal_no_back', 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 scale=(2.1 if uiscale is ba.UIScale.SMALL else
1.5 if uiscale is ba.UIScale.MEDIUM else 1.0), 1.5 if uiscale is ba.UIScale.MEDIUM else 1.0),
scale_origin_stack_offset=scale_origin) scale_origin_stack_offset=scale_origin)
@ -147,12 +147,13 @@ class QuitWindow:
origin_widget=origin_widget).root_widget) origin_widget=origin_widget).root_widget)
def _fade_and_quit(self) -> None: def _fade_and_quit(self) -> None:
_ba.fade_screen(False, ba.internal.fade_screen(
time=0.2, False,
endcall=lambda: ba.quit(soft=True, back=self._back)) time=0.2,
_ba.lock_all_input() endcall=lambda: ba.quit(soft=True, back=self._back))
ba.internal.lock_all_input()
# Unlock and fade back in shortly.. just in case something goes wrong # Unlock and fade back in shortly.. just in case something goes wrong
# (or on android where quit just backs out of our activity and # (or on android where quit just backs out of our activity and
# we may come back) # 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 import weakref
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import _ba
import ba import ba
import ba.internal
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any, Callable from typing import Any, Callable
@ -37,11 +37,14 @@ class ContinuesWindow(ba.Window):
txt = (ba.Lstr( txt = (ba.Lstr(
resource='continuePurchaseText').evaluate().split('${PRICE}')) resource='continuePurchaseText').evaluate().split('${PRICE}'))
t_left = txt[0] 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 = 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 = 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 width_total_half = (t_left_width + t_price_width + t_right_width) * 0.5
ba.textwidget(parent=self._root_widget, ba.textwidget(parent=self._root_widget,
@ -133,8 +136,15 @@ class ContinuesWindow(ba.Window):
ba.WeakCall(self._tick), ba.WeakCall(self._tick),
repeat=True, repeat=True,
timetype=ba.TimeType.REAL) timetype=ba.TimeType.REAL)
# If there is foreground activity, suspend it.
ba.app.pause()
self._tick() self._tick()
def __del__(self) -> None:
# If there is suspended foreground activity, resume it.
ba.app.resume()
def _tick(self) -> None: def _tick(self) -> None:
# if our target activity is gone or has ended, go away # if our target activity is gone or has ended, go away
activity = self._activity() activity = self._activity()
@ -142,9 +152,9 @@ class ContinuesWindow(ba.Window):
self._on_cancel() self._on_cancel()
return return
if _ba.get_v1_account_state() == 'signed_in': if ba.internal.get_v1_account_state() == 'signed_in':
sval = (ba.charstr(ba.SpecialChar.TICKET) + sval = (ba.charstr(ba.SpecialChar.TICKET) +
str(_ba.get_v1_account_ticket_count())) str(ba.internal.get_v1_account_ticket_count()))
else: else:
sval = '?' sval = '?'
if self._tickets_text is not None: if self._tickets_text is not None:
@ -176,14 +186,14 @@ class ContinuesWindow(ba.Window):
ba.playsound(ba.getsound('error')) ba.playsound(ba.getsound('error'))
else: else:
# If somehow we got signed out... # 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'), ba.screenmessage(ba.Lstr(resource='notSignedInText'),
color=(1, 0, 0)) color=(1, 0, 0))
ba.playsound(ba.getsound('error')) ba.playsound(ba.getsound('error'))
return return
# If it appears we don't have enough tickets, offer to buy more. # 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: if tickets < self._cost:
# FIXME: Should we start the timer back up again after? # FIXME: Should we start the timer back up again after?
self._counting_down = False self._counting_down = False

View file

@ -8,8 +8,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import _ba
import ba import ba
import ba.internal
from bastd.ui.store.button import StoreButton from bastd.ui.store.button import StoreButton
from bastd.ui.league.rankbutton import LeagueRankButton from bastd.ui.league.rankbutton import LeagueRankButton
from bastd.ui.store.browser import StoreBrowserWindow from bastd.ui.store.browser import StoreBrowserWindow
@ -26,7 +26,7 @@ class CoopBrowserWindow(ba.Window):
def _update_corner_button_positions(self) -> None: def _update_corner_button_positions(self) -> None:
uiscale = ba.app.ui.uiscale uiscale = ba.app.ui.uiscale
offs = (-55 if uiscale is ba.UIScale.SMALL 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: if self._league_rank_button is not None:
self._league_rank_button.set_position( self._league_rank_button.set_position(
(self._width - 282 + offs - self._x_inset, self._height - 85 - (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 # 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) # 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, ba.timer(1.0,
lambda: ba.screenmessage( lambda: ba.screenmessage(
ba.Lstr(resource='noTournamentsInTestBuildText'), ba.Lstr(resource='noTournamentsInTestBuildText'),
@ -93,7 +93,7 @@ class CoopBrowserWindow(ba.Window):
self._tourney_data_up_to_date = False 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') 'campaignDifficulty', 'easy')
super().__init__(root_widget=ba.containerwidget( super().__init__(root_widget=ba.containerwidget(
@ -234,7 +234,7 @@ class CoopBrowserWindow(ba.Window):
self._subcontainer: ba.Widget | None = None self._subcontainer: ba.Widget | None = None
# Take note of our account state; we'll refresh later if this changes. # 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. # Same for fg/bg state.
self._fg_state = app.fg_state self._fg_state = app.fg_state
@ -252,7 +252,7 @@ class CoopBrowserWindow(ba.Window):
# starting point. # starting point.
if (app.accounts_v1.account_tournament_list is not None if (app.accounts_v1.account_tournament_list is not None
and app.accounts_v1.account_tournament_list[0] 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 t_id in app.accounts_v1.tournament_info
for t_id in app.accounts_v1.account_tournament_list[1])): for t_id in app.accounts_v1.account_tournament_list[1])):
tourney_data = [ tourney_data = [
@ -300,7 +300,7 @@ class CoopBrowserWindow(ba.Window):
self._tourney_data_up_to_date = False self._tourney_data_up_to_date = False
# If our account state has changed, do a full request. # 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: if account_state_num != self._account_state_num:
self._account_state_num = account_state_num self._account_state_num = account_state_num
self._save_state() self._save_state()
@ -324,7 +324,7 @@ class CoopBrowserWindow(ba.Window):
self._fg_state = ba.app.fg_state self._fg_state = ba.app.fg_state
self._last_tournament_query_time = cur_time self._last_tournament_query_time = cur_time
self._doing_tournament_query = True self._doing_tournament_query = True
_ba.tournament_query( ba.internal.tournament_query(
args={ args={
'source': 'coop window refresh', 'source': 'coop window refresh',
'numScores': 1 'numScores': 1
@ -333,7 +333,7 @@ class CoopBrowserWindow(ba.Window):
) )
# Decrement time on our tournament buttons. # 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: for tbtn in self._tournament_buttons:
tbtn.time_remaining = max(0, tbtn.time_remaining - 1) tbtn.time_remaining = max(0, tbtn.time_remaining - 1)
if tbtn.time_remaining_value_text is not None: 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 '-') and self._tourney_data_up_to_date) else '-')
# Also adjust the ad icon visibility. # 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, ba.imagewidget(edit=tbtn.entry_fee_ad_image,
opacity=1.0 if ads_enabled else 0.25) opacity=1.0 if ads_enabled else 0.25)
ba.textwidget(edit=tbtn.entry_fee_text_remaining, ba.textwidget(edit=tbtn.entry_fee_text_remaining,
@ -395,11 +395,9 @@ class CoopBrowserWindow(ba.Window):
accounts.cache_tournament_info(tournament_data) accounts.cache_tournament_info(tournament_data)
# Also cache the current tourney list/order for this account. # Also cache the current tourney list/order for this account.
accounts.account_tournament_list = (_ba.get_v1_account_state_num(), accounts.account_tournament_list = (
[ ba.internal.get_v1_account_state_num(),
e['tournamentID'] [e['tournamentID'] for e in tournament_data])
for e in tournament_data
])
self._doing_tournament_query = False self._doing_tournament_query = False
self._update_for_data(tournament_data) self._update_for_data(tournament_data)
@ -417,7 +415,7 @@ class CoopBrowserWindow(ba.Window):
print('ERROR: invalid campaign difficulty:', difficulty) print('ERROR: invalid campaign difficulty:', difficulty)
difficulty = 'easy' difficulty = 'easy'
self._campaign_difficulty = difficulty self._campaign_difficulty = difficulty
_ba.add_transaction({ ba.internal.add_transaction({
'type': 'SET_MISC_VAL', 'type': 'SET_MISC_VAL',
'name': 'campaignDifficulty', 'name': 'campaignDifficulty',
'value': difficulty 'value': difficulty
@ -638,7 +636,7 @@ class CoopBrowserWindow(ba.Window):
# FIXME shouldn't use hard-coded strings here. # FIXME shouldn't use hard-coded strings here.
txt = ba.Lstr(resource='tournamentsText', txt = ba.Lstr(resource='tournamentsText',
fallback_resource='tournamentText').evaluate() 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, ba.textwidget(parent=w_parent,
position=(h_base + 27, v + 30), position=(h_base + 27, v + 30),
size=(0, 0), size=(0, 0),
@ -668,7 +666,7 @@ class CoopBrowserWindow(ba.Window):
# no tournaments). # no tournaments).
if self._tournament_button_count == 0: if self._tournament_button_count == 0:
unavailable_text = ba.Lstr(resource='unavailableText') 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( unavailable_text = ba.Lstr(
value='${A} (${B})', value='${A} (${B})',
subs=[('${A}', unavailable_text), subs=[('${A}', unavailable_text),
@ -744,8 +742,9 @@ class CoopBrowserWindow(ba.Window):
] ]
# Show easter-egg-hunt either if its easter or we own it. # Show easter-egg-hunt either if its easter or we own it.
if _ba.get_v1_account_misc_read_val( if ba.internal.get_v1_account_misc_read_val(
'easter', False) or _ba.get_purchased('games.easter_egg_hunt'): 'easter',
False) or ba.internal.get_purchased('games.easter_egg_hunt'):
items = [ items = [
'Challenges:Easter Egg Hunt', 'Challenges:Easter Egg Hunt',
'Challenges:Pro Easter Egg Hunt', 'Challenges:Pro Easter Egg Hunt',
@ -838,7 +837,7 @@ class CoopBrowserWindow(ba.Window):
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
from bastd.ui.account import show_sign_in_prompt from bastd.ui.account import show_sign_in_prompt
from bastd.ui.league.rankwindow import LeagueRankWindow 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() show_sign_in_prompt()
return return
self._save_state() self._save_state()
@ -855,7 +854,7 @@ class CoopBrowserWindow(ba.Window):
) -> None: ) -> None:
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
from bastd.ui.account import show_sign_in_prompt 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() show_sign_in_prompt()
return return
self._save_state() self._save_state()
@ -893,7 +892,7 @@ class CoopBrowserWindow(ba.Window):
if game in ('Challenges:Infinite Runaround', if game in ('Challenges:Infinite Runaround',
'Challenges:Infinite Onslaught' 'Challenges:Infinite Onslaught'
) and not ba.app.accounts_v1.have_pro(): ) 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() show_sign_in_prompt()
else: else:
PurchaseWindow(items=['pro']) PurchaseWindow(items=['pro'])
@ -920,8 +919,8 @@ class CoopBrowserWindow(ba.Window):
required_purchase = None required_purchase = None
if (required_purchase is not None if (required_purchase is not None
and not _ba.get_purchased(required_purchase)): and not ba.internal.get_purchased(required_purchase)):
if _ba.get_v1_account_state() != 'signed_in': if ba.internal.get_v1_account_state() != 'signed_in':
show_sign_in_prompt() show_sign_in_prompt()
else: else:
PurchaseWindow(items=[required_purchase]) PurchaseWindow(items=[required_purchase])
@ -937,10 +936,17 @@ class CoopBrowserWindow(ba.Window):
from bastd.ui.account import show_sign_in_prompt from bastd.ui.account import show_sign_in_prompt
from bastd.ui.tournamententry import TournamentEntryWindow 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() show_sign_in_prompt()
return 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: if not self._tourney_data_up_to_date:
ba.screenmessage(ba.Lstr(resource='tournamentCheckingStateText'), ba.screenmessage(ba.Lstr(resource='tournamentCheckingStateText'),
color=(1, 1, 0)) color=(1, 1, 0))

View file

@ -7,7 +7,6 @@ from __future__ import annotations
import random import random
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import _ba
import ba import ba
if TYPE_CHECKING: if TYPE_CHECKING:
@ -200,17 +199,17 @@ class GameButton:
'Challenges:Infinite Onslaught') 'Challenges:Infinite Onslaught')
and not ba.app.accounts_v1.have_pro()) and not ba.app.accounts_v1.have_pro())
or (game in ('Challenges:Meteor Shower', ) 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', or (game in ('Challenges:Target Practice',
'Challenges:Target Practice B') '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', ) 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', ) or (game in ('Challenges:Pro Ninja Fight', )
and not _ba.get_purchased('games.ninja_fight')) and not ba.internal.get_purchased('games.ninja_fight')) or
or (game in ('Challenges:Easter Egg Hunt', (game in ('Challenges:Easter Egg Hunt',
'Challenges:Pro 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 unlocked = False
# Let's tint levels a slightly different color when easy mode # 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 copy
import ba import ba
import _ba import ba.internal
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any, Callable from typing import Any, Callable
@ -499,7 +499,7 @@ class TournamentButton:
self.allow_ads = allow_ads = entry['allowAds'] self.allow_ads = allow_ads = entry['allowAds']
final_fee: int | None = (None if fee_var is None else 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, '?')) fee_var, '?'))
final_fee_str: str | ba.Lstr final_fee_str: str | ba.Lstr
@ -519,8 +519,8 @@ class TournamentButton:
# Now, if this fee allows ads and we support video ads, show # Now, if this fee allows ads and we support video ads, show
# the 'or ad' version. # the 'or ad' version.
if allow_ads and _ba.has_video_ads(): if allow_ads and ba.internal.has_video_ads():
ads_enabled = _ba.have_incentivized_ad() ads_enabled = ba.internal.have_incentivized_ad()
ba.imagewidget(edit=self.entry_fee_ad_image, ba.imagewidget(edit=self.entry_fee_ad_image,
opacity=1.0 if ads_enabled else 0.25) opacity=1.0 if ads_enabled else 0.25)
or_text = ba.Lstr(resource='orText', or_text = ba.Lstr(resource='orText',

View file

@ -6,8 +6,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import _ba
import ba import ba
import ba.internal
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Sequence from typing import Sequence
@ -91,17 +91,19 @@ class CreditsListWindow(ba.Window):
capture_arrows=True) capture_arrows=True)
if ba.app.ui.use_toolbars: if ba.app.ui.use_toolbars:
ba.widget(edit=scroll, ba.widget(
right_widget=_ba.get_special_widget('party_button')) edit=scroll,
right_widget=ba.internal.get_special_widget('party_button'))
if uiscale is ba.UIScale.SMALL: if uiscale is ba.UIScale.SMALL:
ba.widget(edit=scroll, ba.widget(
left_widget=_ba.get_special_widget('back_button')) edit=scroll,
left_widget=ba.internal.get_special_widget('back_button'))
def _format_names(names2: Sequence[str], inset: float) -> str: def _format_names(names2: Sequence[str], inset: float) -> str:
sval = '' sval = ''
# measure a series since there's overlaps and stuff.. # measure a series since there's overlaps and stuff..
space_width = _ba.get_string_width(' ' * 10, space_width = ba.internal.get_string_width(
suppress_warning=True) / 10.0 ' ' * 10, suppress_warning=True) / 10.0
spacing = 330.0 spacing = 330.0
col1 = inset col1 = inset
col2 = col1 + spacing col2 = col1 + spacing
@ -124,7 +126,8 @@ class CreditsListWindow(ba.Window):
spacingstr = ' ' * int((target - line_width) / space_width) spacingstr = ' ' * int((target - line_width) / space_width)
nline += spacingstr nline += spacingstr
nline += name 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 != '': if nline != '':
sval += nline + '\n' sval += nline + '\n'
return sval return sval
@ -236,7 +239,7 @@ class CreditsListWindow(ba.Window):
'${NAME}', 'the Khronos Group') + '\n' '${NAME}', 'the Khronos Group') + '\n'
'\n' '\n'
' ' ' '
' www.froemling.net\n') ' www.ballistica.net\n')
txt = credits_text txt = credits_text
lines = txt.splitlines() lines = txt.splitlines()

View file

@ -15,11 +15,12 @@ if TYPE_CHECKING:
class DebugWindow(ba.Window): class DebugWindow(ba.Window):
"""Window for debugging internal values.""" """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=too-many-statements
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
from bastd.ui import popup from bastd.ui import popup
ba.app.ui.set_main_menu_location('Benchmarks & Stress Tests')
uiscale = ba.app.ui.uiscale uiscale = ba.app.ui.uiscale
self._width = width = 580 self._width = width = 580
self._height = height = (350 if uiscale is ba.UIScale.SMALL else 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 from typing import TYPE_CHECKING
import ba import ba
import ba.internal
if TYPE_CHECKING: if TYPE_CHECKING:
pass pass
@ -54,13 +55,12 @@ def ask_for_rating() -> ba.Widget | None:
v_align='center') v_align='center')
def do_rating() -> None: def do_rating() -> None:
import _ba
if platform == 'android': if platform == 'android':
appname = _ba.appname() appname = ba.internal.appname()
if subplatform == 'google': if subplatform == 'google':
url = f'market://details?id=net.froemling.{appname}' url = f'market://details?id=net.froemling.{appname}'
else: else:
url = 'market://details?id=net.froemling.{appname}cb' url = f'market://details?id=net.froemling.{appname}cb'
else: else:
url = 'macappstore://itunes.apple.com/app/id416482767?ls=1&mt=12' url = 'macappstore://itunes.apple.com/app/id416482767?ls=1&mt=12'

View file

@ -9,8 +9,8 @@ import threading
import time import time
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import _ba
import ba import ba
import ba.internal
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any, Callable, Sequence from typing import Any, Callable, Sequence
@ -242,7 +242,7 @@ class FileSelectorWindow(ba.Window):
max_str_width = 300.0 max_str_width = 300.0
str_width = min( str_width = min(
max_str_width, 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, ba.textwidget(edit=self._path_text,
text=folder_name, text=folder_name,
maxwidth=max_str_width) maxwidth=max_str_width)

View file

@ -8,8 +8,8 @@ import weakref
from enum import Enum from enum import Enum
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import _ba
import ba import ba
import ba.internal
from bastd.ui.tabs import TabRow from bastd.ui.tabs import TabRow
if TYPE_CHECKING: if TYPE_CHECKING:
@ -88,7 +88,7 @@ class GatherWindow(ba.Window):
self._transition_out = 'out_right' self._transition_out = 'out_right'
scale_origin = None scale_origin = None
ba.app.ui.set_main_menu_location('Gather') 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 uiscale = ba.app.ui.uiscale
self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040 self._width = 1240 if uiscale is ba.UIScale.SMALL else 1040
x_offs = 100 if uiscale is ba.UIScale.SMALL else 0 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]] = [ tabdefs: list[tuple[GatherWindow.TabID, ba.Lstr]] = [
(self.TabID.ABOUT, ba.Lstr(resource=self._r + '.aboutText')) (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, tabdefs.append((self.TabID.INTERNET,
ba.Lstr(resource=self._r + '.publicText'))) ba.Lstr(resource=self._r + '.publicText')))
tabdefs.append( tabdefs.append(
@ -186,11 +187,13 @@ class GatherWindow(ba.Window):
self._tabs[tab_id] = tabtype(self) self._tabs[tab_id] = tabtype(self)
if ba.app.ui.use_toolbars: if ba.app.ui.use_toolbars:
ba.widget(edit=self._tab_row.tabs[tabdefs[-1][0]].button, ba.widget(
right_widget=_ba.get_special_widget('party_button')) edit=self._tab_row.tabs[tabdefs[-1][0]].button,
right_widget=ba.internal.get_special_widget('party_button'))
if uiscale is ba.UIScale.SMALL: if uiscale is ba.UIScale.SMALL:
ba.widget(edit=self._tab_row.tabs[tabdefs[0][0]].button, ba.widget(
left_widget=_ba.get_special_widget('back_button')) 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_width = self._width - scroll_buffer_h
self._scroll_height = self._height - 180.0 + tabs_top_extra self._scroll_height = self._height - 180.0 + tabs_top_extra
@ -214,7 +217,7 @@ class GatherWindow(ba.Window):
self._restore_state() self._restore_state()
def __del__(self) -> None: 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: def playlist_select(self, origin_widget: ba.Widget) -> None:
"""Called by the private-hosting tab to select a playlist.""" """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 from typing import TYPE_CHECKING
import ba import ba
import _ba import ba.internal
from bastd.ui.gather import GatherTab from bastd.ui.gather import GatherTab
if TYPE_CHECKING: if TYPE_CHECKING:
@ -51,8 +51,8 @@ class AboutGatherTab(GatherTab):
include_invite = True include_invite = True
msc_scale = 1.1 msc_scale = 1.1
c_height_2 = min(region_height, string_height * msc_scale + 100) c_height_2 = min(region_height, string_height * msc_scale + 100)
try_tickets = _ba.get_v1_account_misc_read_val('friendTryTickets', try_tickets = ba.internal.get_v1_account_misc_read_val(
None) 'friendTryTickets', None)
if try_tickets is None: if try_tickets is None:
include_invite = False include_invite = False
self._container = ba.containerwidget( self._container = ba.containerwidget(
@ -106,7 +106,7 @@ class AboutGatherTab(GatherTab):
def _invite_to_try_press(self) -> None: def _invite_to_try_press(self) -> None:
from bastd.ui.account import show_sign_in_prompt from bastd.ui.account import show_sign_in_prompt
from bastd.ui.appinvite import handle_app_invites_press 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() show_sign_in_prompt()
return return
handle_app_invites_press() handle_app_invites_press()

View file

@ -11,8 +11,8 @@ from enum import Enum
from dataclasses import dataclass from dataclasses import dataclass
from bastd.ui.gather import GatherTab from bastd.ui.gather import GatherTab
import _ba
import ba import ba
import ba.internal
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any, Callable from typing import Any, Callable
@ -341,8 +341,9 @@ class ManualGatherTab(GatherTab):
label=ba.Lstr(resource='gatherWindow.manualConnectText'), label=ba.Lstr(resource='gatherWindow.manualConnectText'),
autoselect=True) autoselect=True)
if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars:
ba.widget(edit=btn1, ba.widget(
left_widget=_ba.get_special_widget('back_button')) edit=btn1,
left_widget=ba.internal.get_special_widget('back_button'))
btnv -= b_height + b_space_extra btnv -= b_height + b_space_extra
ba.buttonwidget(parent=self._container, ba.buttonwidget(parent=self._container,
size=(b_width, b_height), size=(b_width, b_height),
@ -686,7 +687,7 @@ class ManualGatherTab(GatherTab):
config = ba.app.config config = ba.app.config
config['Last Manual Party Connect Address'] = resolved_address config['Last Manual Party Connect Address'] = resolved_address
config.commit() 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: def _run_addr_fetch(self) -> None:
try: try:
@ -894,9 +895,12 @@ class ManualGatherTab(GatherTab):
if t_accessible_extra: if t_accessible_extra:
ba.textwidget( ba.textwidget(
edit=t_accessible_extra, edit=t_accessible_extra,
text=ba.Lstr(resource='gatherWindow.' text=ba.Lstr(
'manualRouterForwardingText', resource='gatherWindow.'
subs=[('${PORT}', 'manualRouterForwardingText',
str(_ba.get_game_port()))]), subs=[
('${PORT}', str(ba.internal.get_game_port())),
],
),
color=color_bad, color=color_bad,
) )

View file

@ -8,7 +8,7 @@ import weakref
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import ba import ba
import _ba import ba.internal
from bastd.ui.gather import GatherTab from bastd.ui.gather import GatherTab
if TYPE_CHECKING: if TYPE_CHECKING:
@ -42,13 +42,13 @@ class NetScanner:
ba.timer(0.25, ba.WeakCall(self.update), timetype=ba.TimeType.REAL) ba.timer(0.25, ba.WeakCall(self.update), timetype=ba.TimeType.REAL)
def __del__(self) -> None: def __del__(self) -> None:
_ba.end_host_scanning() ba.internal.end_host_scanning()
def _on_select(self, host: dict[str, Any]) -> None: def _on_select(self, host: dict[str, Any]) -> None:
self._last_selected_host = host self._last_selected_host = host
def _on_activate(self, host: dict[str, Any]) -> None: 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: def update(self) -> None:
"""(internal)""" """(internal)"""
@ -65,7 +65,7 @@ class NetScanner:
# Grab this now this since adding widgets will change it. # Grab this now this since adding widgets will change it.
last_selected_host = self._last_selected_host last_selected_host = self._last_selected_host
hosts = _ba.host_scan_cycle() hosts = ba.internal.host_scan_cycle()
for i, host in enumerate(hosts): for i, host in enumerate(hosts):
txt3 = ba.textwidget(parent=self._columnwidget, txt3 = ba.textwidget(parent=self._columnwidget,
size=(self._width / t_scale, 30), size=(self._width / t_scale, 30),

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