This commit is contained in:
Ayush Saini 2025-09-15 18:52:19 +00:00 committed by GitHub
commit 7b77e705d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
200 changed files with 9971 additions and 4452 deletions

View file

@ -5,6 +5,7 @@ on:
branches: branches:
- public-server - public-server
- api9 - api9
- dev
jobs: jobs:
run_server_binary: run_server_binary:

View file

@ -20,6 +20,7 @@
"Indonesian": "Bahasa Indonesia", "Indonesian": "Bahasa Indonesia",
"Italian": "Italiano", "Italian": "Italiano",
"Japanese": "日本語", "Japanese": "日本語",
"Kazakh": "Қазақша",
"Korean": "한국어", "Korean": "한국어",
"Malay": "Melayu", "Malay": "Melayu",
"Persian": "⁦فارسی‎", "Persian": "⁦فارسی‎",
@ -102,6 +103,7 @@
"Gifasa abidjahsi", "Gifasa abidjahsi",
"Abinav", "Abinav",
"Abir", "Abir",
"Abishek",
"ABITDANTON", "ABITDANTON",
"Abne", "Abne",
"Abolfadl", "Abolfadl",
@ -114,6 +116,7 @@
"adan", "adan",
"Adeel (AdeZ {@adez_})", "Adeel (AdeZ {@adez_})",
"Adel", "Adel",
"adeline",
"AdemYzz", "AdemYzz",
"Rio adi", "Rio adi",
"Rayhan Adiansyah", "Rayhan Adiansyah",
@ -284,6 +287,7 @@
"Asshold", "Asshold",
"Eliane Santos de assis", "Eliane Santos de assis",
"Atalanta", "Atalanta",
"Atayk00",
"Atilla", "Atilla",
"Atom", "Atom",
"Attila", "Attila",
@ -311,6 +315,7 @@
"Myth B.", "Myth B.",
"B4likeBefore", "B4likeBefore",
"Praveen Babu", "Praveen Babu",
"Baby🇭🇹",
"Badmoss", "Badmoss",
"Baechu", "Baechu",
"bag", "bag",
@ -327,6 +332,7 @@
"Ibrahim Baraka", "Ibrahim Baraka",
"Kamil Barański (Limak09)", "Kamil Barański (Limak09)",
"Leonan Barcelos", "Leonan Barcelos",
"Bardak56",
"Bardiaghasedipour", "Bardiaghasedipour",
"William Barnak", "William Barnak",
"William Barnakk", "William Barnakk",
@ -372,6 +378,7 @@
"Anton Bang Berner", "Anton Bang Berner",
"Felix Bernhard", "Felix Bernhard",
"Beroudzin", "Beroudzin",
"Besho",
"Abhishek Bhardwaj", "Abhishek Bhardwaj",
"Bhxyu", "Bhxyu",
"Arthur Bianco", "Arthur Bianco",
@ -403,6 +410,7 @@
"Gianfranco Del Borrello", "Gianfranco Del Borrello",
"Abel Borso", "Abel Borso",
"Plasma Boson", "Plasma Boson",
"Cristi bossul",
"Cristian Bote \"ZZAZZ\"", "Cristian Bote \"ZZAZZ\"",
"Cristián Bote", "Cristián Bote",
"botris", "botris",
@ -473,6 +481,7 @@
"Ceren", "Ceren",
"cflagos", "cflagos",
"cgaming", "cgaming",
"Chammmbeo",
"chang", "chang",
"Charlie", "Charlie",
"Pradip Chaudhari", "Pradip Chaudhari",
@ -575,6 +584,8 @@
"df", "df",
"DFгульСпачибо", "DFгульСпачибо",
"Santanu Dhar", "Santanu Dhar",
"Dheeraj",
"Dhei8dje3",
"DHRUVIL", "DHRUVIL",
"DIAbli", "DIAbli",
"Diablo", "Diablo",
@ -640,6 +651,7 @@
"EgorZH", "EgorZH",
"Ali ehs", "Ali ehs",
"eightyfoahh", "eightyfoahh",
"EiOi",
"Eiva", "Eiva",
"EK", "EK",
"EKFH", "EKFH",
@ -652,6 +664,7 @@
"ElDemon", "ElDemon",
"ElderLink", "ElderLink",
"elfree", "elfree",
"Peter Elia",
"Elian", "Elian",
"Elmakyt", "Elmakyt",
"ELMEX95", "ELMEX95",
@ -680,6 +693,7 @@
"Bueno pero igual era", "Bueno pero igual era",
"Era0S (Spazton)", "Era0S (Spazton)",
"EraOS", "EraOS",
"Erenbabapro",
"Erfan", "Erfan",
"Eric-fan", "Eric-fan",
"Erick", "Erick",
@ -699,6 +713,7 @@
"EXTENDOO", "EXTENDOO",
"Eyder", "Eyder",
"Eymen", "Eymen",
"Leen Ezrin",
"F15fahd_lol", "F15fahd_lol",
"fa9oly9", "fa9oly9",
"Fabian", "Fabian",
@ -855,6 +870,7 @@
"Hack", "Hack",
"HackPlayer697", "HackPlayer697",
"hadi", "hadi",
"Hafod",
"hafzanpajan", "hafzanpajan",
"Haidar", "Haidar",
"Joud haidar", "Joud haidar",
@ -864,6 +880,7 @@
"HamCam1015", "HamCam1015",
"hamed", "hamed",
"Alhasan Hamoud/Alretrox ❤️‍🔥🖤", "Alhasan Hamoud/Alretrox ❤️‍🔥🖤",
"HamzaDriss",
"Zulfikar Hanif", "Zulfikar Hanif",
"Happaphus", "Happaphus",
"Franschieko Satya Haprabu", "Franschieko Satya Haprabu",
@ -874,6 +891,7 @@
"Hasan", "Hasan",
"Mohammad hasan", "Mohammad hasan",
"Hisham bin Hashim", "Hisham bin Hashim",
"Hashtag",
"Emil Hauge", "Emil Hauge",
"Arian Haxhijaj", "Arian Haxhijaj",
"Ergin Haxhijaj", "Ergin Haxhijaj",
@ -934,6 +952,7 @@
"Hussam", "Hussam",
"Huy", "Huy",
"HyMr", "HyMr",
"Popsie (formely HyMr)",
"HYr", "HYr",
"Adrian Höfer", "Adrian Höfer",
"Davide Iaccarino", "Davide Iaccarino",
@ -967,6 +986,7 @@
"indieGEARgames", "indieGEARgames",
"Darkness indo", "Darkness indo",
"Indohuman", "Indohuman",
"Infray",
"Inicio", "Inicio",
"IniSaya6666", "IniSaya6666",
"inkMedic", "inkMedic",
@ -1040,7 +1060,9 @@
"Steven john", "Steven john",
"Johnny", "Johnny",
"Johnwick", "Johnwick",
"johnyzzh",
"joke", "joke",
"JolteonLover2050Electric",
"jonas-bonas", "jonas-bonas",
"Jonatas", "Jonatas",
"Jonathan", "Jonathan",
@ -1089,12 +1111,15 @@
"Karim", "Karim",
"Karlimero", "Karlimero",
"shemas - Ebrahim Karram", "shemas - Ebrahim Karram",
"karry",
"Kasra", "Kasra",
"katon", "katon",
"Kau5hik", "Kau5hik",
"Alex Kaufman",
"Kaunyt", "Kaunyt",
"Kaushik", "Kaushik",
"KawaiiON", "KawaiiON",
"KAZDOG",
"kazoo081", "kazoo081",
"kazooicek", "kazooicek",
"KD", "KD",
@ -1191,6 +1216,7 @@
"Lemon4ik", "Lemon4ik",
"Leo", "Leo",
"Mr. LeoLeo", "Mr. LeoLeo",
"Leonar",
"Leonid", "Leonid",
"Lepixhd", "Lepixhd",
"Lester", "Lester",
@ -1207,6 +1233,7 @@
"Nicola Ligas", "Nicola Ligas",
"Alef costa lima", "Alef costa lima",
"Limak09", "Limak09",
"Limber",
"LimonAga", "LimonAga",
"lin", "lin",
"Dustin Lin", "Dustin Lin",
@ -1362,6 +1389,7 @@
"Medic药", "Medic药",
"German Medin", "German Medin",
"Martin Medina", "Martin Medina",
"Mega",
"Mehret Mehanzel", "Mehret Mehanzel",
"Mobin Mehdizadeh", "Mobin Mehdizadeh",
"Mehmet", "Mehmet",
@ -1416,6 +1444,7 @@
"Mohammad11dembele", "Mohammad11dembele",
"MOHAMMADERFAN", "MOHAMMADERFAN",
"Mohammadhosain", "Mohammadhosain",
"MohammadMardi",
"Mohammadpl", "Mohammadpl",
"Mohammed", "Mohammed",
"MohammedTalal1st", "MohammedTalal1st",
@ -1447,6 +1476,7 @@
"MrDaniel715", "MrDaniel715",
"MrGlu10free", "MrGlu10free",
"Mrmaxmeier", "Mrmaxmeier",
"MrMeme",
"MrNexis", "MrNexis",
"MrS0meone", "MrS0meone",
"MrSaster2024", "MrSaster2024",
@ -1534,6 +1564,7 @@
"NoNameA14171274.", "NoNameA14171274.",
"NoNameC3698241", "NoNameC3698241",
"None", "None",
"Nonspe 1120",
"NOOBPEDAR", "NOOBPEDAR",
"NoobPilotPlayz", "NoobPilotPlayz",
"Noobslaya101", "Noobslaya101",
@ -1562,6 +1593,7 @@
"On3GaMs", "On3GaMs",
"No one", "No one",
"oneman", "oneman",
"Ornstein",
"Adam Oros", "Adam Oros",
"Andrés Ortega", "Andrés Ortega",
"Zangar Orynbetov", "Zangar Orynbetov",
@ -1655,6 +1687,7 @@
"Pong", "Pong",
"Lehlogonolo \"YetNT\" Poole", "Lehlogonolo \"YetNT\" Poole",
"Pooya", "Pooya",
"Popsie",
"pouriya", "pouriya",
"Pouya", "Pouya",
"pranav", "pranav",
@ -1762,12 +1795,14 @@
"Rishabh", "Rishabh",
"Rivki", "Rivki",
"rizaldy", "rizaldy",
"Robert",
"Rodbert", "Rodbert",
"Rodrigo", "Rodrigo",
"Giovanni Rodríguez", "Giovanni Rodríguez",
"Marco Rodríguez", "Marco Rodríguez",
"Rohan", "Rohan",
"Rohit", "Rohit",
"rojman",
"Bihary Roland", "Bihary Roland",
"Jericho roldan", "Jericho roldan",
"Roma :D", "Roma :D",
@ -1790,8 +1825,10 @@
"RussianLover2008", "RussianLover2008",
"Ryan", "Ryan",
"LiÇViN:Cviatkoú Kanstançin Rygoravič", "LiÇViN:Cviatkoú Kanstançin Rygoravič",
"Šimon S.",
"Ricky Joe S.Flores", "Ricky Joe S.Flores",
"Rami Sabbagh", "Rami Sabbagh",
"Sadramj91",
"Justin Saephan", "Justin Saephan",
"sahel", "sahel",
"Abdullah Saim", "Abdullah Saim",
@ -1806,6 +1843,7 @@
"Salted", "Salted",
"Matteo Salvini", "Matteo Salvini",
"Salvo04", "Salvo04",
"Sam",
"SamComeli", "SamComeli",
"Ali Sameer", "Ali Sameer",
"Samen", "Samen",
@ -1820,6 +1858,7 @@
"Guilherme Santana", "Guilherme Santana",
"Santiago", "Santiago",
"Ivan Santos :)", "Ivan Santos :)",
"Nicolas Vicente dos Santos",
"santosamerica880@gmail.com", "santosamerica880@gmail.com",
"Diamond Sanwich", "Diamond Sanwich",
"SAO_OMH", "SAO_OMH",
@ -1883,6 +1922,7 @@
"Slavik❤", "Slavik❤",
"SlayTaniK", "SlayTaniK",
"slcat", "slcat",
"Slmnd",
"Igor Slobodchuk", "Igor Slobodchuk",
"Rasim Smaili", "Rasim Smaili",
"Nicola Smaniotto", "Nicola Smaniotto",
@ -1894,6 +1934,9 @@
"Matheus Soares", "Matheus Soares",
"sobhan", "sobhan",
"Nikhil sohan", "Nikhil sohan",
"Soheib",
"SoheibAk",
"soheil",
"SoK", "SoK",
"SoldierBS", "SoldierBS",
"Unnamed Solicitude", "Unnamed Solicitude",
@ -2006,6 +2049,7 @@
"Cristian Ticu", "Cristian Ticu",
"Robert Tieber", "Robert Tieber",
"TieDan", "TieDan",
"250 Tier",
"Tigas", "Tigas",
"TIGEE", "TIGEE",
"Tim", "Tim",
@ -2015,6 +2059,7 @@
"tjkffndeupwfbkh", "tjkffndeupwfbkh",
"Juraj Tlach", "Juraj Tlach",
"TM-DoDo", "TM-DoDo",
"Tocinito",
"Toloche", "Toloche",
"Tom", "Tom",
"Juan Pablo Montoya Tomalá", "Juan Pablo Montoya Tomalá",
@ -2060,6 +2105,7 @@
"Usishshsis", "Usishshsis",
"utyrrwq", "utyrrwq",
"Uzinerz", "Uzinerz",
"Uzma",
"Uładzisłaŭ", "Uładzisłaŭ",
"Shohrux V", "Shohrux V",
"Vader", "Vader",
@ -2085,6 +2131,7 @@
"vijay", "vijay",
"vinicius", "vinicius",
"Robin Vinith", "Robin Vinith",
"Casper Vinkle",
"vinoth", "vinoth",
"Vishal", "Vishal",
"VISHUUU", "VISHUUU",
@ -2109,7 +2156,9 @@
"Wakefield", "Wakefield",
"Simon Wang", "Simon Wang",
"Will Wang", "Will Wang",
"Watermelon",
"WeanCZ", "WeanCZ",
"web",
"Tilman Weber", "Tilman Weber",
"webparham", "webparham",
"Wesley", "Wesley",
@ -2148,6 +2197,7 @@
"xxonx8", "xxonx8",
"Ajeet yadav", "Ajeet yadav",
"Taha yaghobi", "Taha yaghobi",
"Taha yaghoubi",
"yahya", "yahya",
"Arda Yalın", "Arda Yalın",
"Yamir", "Yamir",
@ -2227,12 +2277,14 @@
"zPanxo", "zPanxo",
"ZpeedTube", "ZpeedTube",
"Krivoy Zub", "Krivoy Zub",
"Zuro0",
"Zwizard", "Zwizard",
"zx4571", "zx4571",
"|_Jenqa_|", "|_Jenqa_|",
"¥¥S.A.N.A¥", "¥¥S.A.N.A¥",
"GURLER Çeviri", "GURLER Çeviri",
"Éleftheros", "Éleftheros",
"Berat Öztürk",
"Danijel Ćelić", "Danijel Ćelić",
"Štěpán", "Štěpán",
"Cristian Țicu", "Cristian Țicu",
@ -2443,6 +2495,7 @@
"전감호", "전감호",
"BombsquadKorea 네이버 카페", "BombsquadKorea 네이버 카페",
"F҉a҉d҉l҉i҉n҉e҉t҉", "F҉a҉d҉l҉i҉n҉e҉t҉",
"Gravöx",
"Zona-BombSquad", "Zona-BombSquad",
"༺Leͥgeͣnͫd༻", "༺Leͥgeͣnͫd༻",
"CrazySquad", "CrazySquad",

View file

@ -1639,6 +1639,7 @@
"Indonesian": "الأندونيسية", "Indonesian": "الأندونيسية",
"Italian": "الإيطالية", "Italian": "الإيطالية",
"Japanese": "اليابانية", "Japanese": "اليابانية",
"Kazakh": "الكازاخية",
"Korean": "الكورية", "Korean": "الكورية",
"Malay": "لغة الملايو", "Malay": "لغة الملايو",
"Persian": "الفارسية", "Persian": "الفارسية",
@ -1772,6 +1773,7 @@
"You got an achievement reward!": "لقد حصلت على جائزة إنجاز!", "You got an achievement reward!": "لقد حصلت على جائزة إنجاز!",
"You have been promoted to a new league; congratulations!": "لقد تم ترقيتك إلى الدوري الجديد. تهانينا!", "You have been promoted to a new league; congratulations!": "لقد تم ترقيتك إلى الدوري الجديد. تهانينا!",
"You lost a chest! (All your chest slots were full)": "خسرت كنز! (كل فتحات الكنوز كانت ممتلئة)", "You lost a chest! (All your chest slots were full)": "خسرت كنز! (كل فتحات الكنوز كانت ممتلئة)",
"You must sign in to do this.": "عليك تسجيل الدخول للقيام بهذا",
"You must update the app to view this.": "يجب عليك تحديث التطبيق لمشاهدة هذا", "You must update the app to view this.": "يجب عليك تحديث التطبيق لمشاهدة هذا",
"You must update to a newer version of the app to do this.": "يجب تحديث إلى إصدار أحدث من التطبيق للقيام بذلك.", "You must update to a newer version of the app to do this.": "يجب تحديث إلى إصدار أحدث من التطبيق للقيام بذلك.",
"You must update to the newest version of the game to do this.": "يجب عليك التحديث إلى الإصدار الأحدث من اللعبة للقيام بذلك.", "You must update to the newest version of the game to do this.": "يجب عليك التحديث إلى الإصدار الأحدث من اللعبة للقيام بذلك.",

View file

@ -1635,7 +1635,8 @@
"Arabic": "Арабскi", "Arabic": "Арабскi",
"Belarussian": "Беларуская", "Belarussian": "Беларуская",
"Chinese": "Кітайская спрошчаная", "Chinese": "Кітайская спрошчаная",
"ChineseTraditional": "Кітайская традыцыйная", "ChineseSimplified": "Кітайская - Спрошчаная",
"ChineseTraditional": "Кітайская - Традыцыіная",
"Croatian": "Харвацкая", "Croatian": "Харвацкая",
"Czech": "Чэшская", "Czech": "Чэшская",
"Danish": "Дацкая", "Danish": "Дацкая",
@ -1646,7 +1647,7 @@
"Finnish": "Фінская", "Finnish": "Фінская",
"French": "Французская", "French": "Французская",
"German": "Нямецкая", "German": "Нямецкая",
"Gibberish": "Gibberish", "Gibberish": "Брэхталі",
"Greek": "Грэчаскі", "Greek": "Грэчаскі",
"Hindi": "Хіндзі", "Hindi": "Хіндзі",
"Hungarian": "Венгерская", "Hungarian": "Венгерская",
@ -1659,11 +1660,15 @@
"PirateSpeak": "Пірацкая мова", "PirateSpeak": "Пірацкая мова",
"Polish": "Польская", "Polish": "Польская",
"Portuguese": "Партугальская", "Portuguese": "Партугальская",
"PortugueseBrazil": "Партугальская - Бразілія",
"PortuguesePortugal": "Партугальская - Партугалія",
"Romanian": "Румынская", "Romanian": "Румынская",
"Russian": "Руская", "Russian": "Руская",
"Serbian": "Сербская", "Serbian": "Сербская",
"Slovak": "Славацкая", "Slovak": "Славацкая",
"Spanish": "Гішпанская", "Spanish": "Гішпанская",
"SpanishLatinAmerica": "Іспанская - Лацінская Амерыка",
"SpanishSpain": "Іспанская - Іспанія",
"Swedish": "Шведская", "Swedish": "Шведская",
"Tamil": "тамільская", "Tamil": "тамільская",
"Thai": "Тайская мова", "Thai": "Тайская мова",

View file

@ -1444,7 +1444,7 @@
"tokenPack2Text": "中型炸币包", "tokenPack2Text": "中型炸币包",
"tokenPack3Text": "大型炸币包", "tokenPack3Text": "大型炸币包",
"tokenPack4Text": "超大炸币包", "tokenPack4Text": "超大炸币包",
"tokensDescriptionText": "代币用于加速胸部解锁以及其他游戏和帐户功能。\n您可以在游戏中赢得代币\n\n也可以打包购买。\n或者购买无限代币的Gold Pass\n再也不会听说它们了。", "tokensDescriptionText": "代币用于加速解锁宝箱以及其他游戏和帐户功能。\n您可以在游戏中赢得代币\n\n也可以打包购买。\n或者购买无限代币的Gold Pass\n再也不会听说它们了。",
"youHaveGoldPassText": "你获得了黄金通行证\n所有花销全部免费啦\n感谢你游玩本游戏" "youHaveGoldPassText": "你获得了黄金通行证\n所有花销全部免费啦\n感谢你游玩本游戏"
}, },
"topFriendsText": "最佳好友", "topFriendsText": "最佳好友",
@ -1621,6 +1621,7 @@
"Indonesian": "印尼语", "Indonesian": "印尼语",
"Italian": "意大利语", "Italian": "意大利语",
"Japanese": "日本语", "Japanese": "日本语",
"Kazakh": "哈萨克语",
"Korean": "朝鲜语", "Korean": "朝鲜语",
"Malay": "马来语", "Malay": "马来语",
"Persian": "波斯文", "Persian": "波斯文",

View file

@ -374,14 +374,14 @@
"chatUnMuteText": "取消屏蔽消息", "chatUnMuteText": "取消屏蔽消息",
"chests": { "chests": {
"prizeOddsText": "中獎機率", "prizeOddsText": "中獎機率",
"reduceWaitText": "減少等待", "reduceWaitText": "減少等待時間",
"slotDescriptionText": "這個插槽可以容納一個箱子。\n\n透過玩戰役關卡贏取寶箱\n參加比賽並完成\n成就。", "slotDescriptionText": "這個插槽可以容納一個箱子。\n\n透過玩戰役關卡贏取寶箱\n參加比賽並完成\n成就。",
"slotText": "寶箱槽 ${NUM}", "slotText": "寶箱槽 ${NUM}",
"slotsFullWarningText": "警告:您的所有寶箱槽都已滿。\n您在本遊戲中獲得的所有寶箱都將遺失。", "slotsFullWarningText": "警告:您的所有寶箱槽都已滿。\n您在本遊戲中獲得的所有寶箱都將遺失。",
"unlocksInText": "解鎖" "unlocksInText": "距離解鎖剩餘"
}, },
"choosingPlayerText": "<選擇玩家>", "choosingPlayerText": "<選擇玩家>",
"claimText": "宣稱", "claimText": "領取",
"codesExplainText": "代碼由開發者提供\n以診斷及改正帳戶問題。", "codesExplainText": "代碼由開發者提供\n以診斷及改正帳戶問題。",
"completeThisLevelToProceedText": "你需要先完成\n這一關", "completeThisLevelToProceedText": "你需要先完成\n這一關",
"completionBonusText": "完成獎勵", "completionBonusText": "完成獎勵",
@ -1072,9 +1072,9 @@
"merchText": "周邊", "merchText": "周邊",
"modeArcadeText": "街機模式", "modeArcadeText": "街機模式",
"modeClassicText": "經典模式", "modeClassicText": "經典模式",
"modeDemoText": "示模式", "modeDemoText": "模式",
"moreSoonText": "更多內容即將推出...", "moreSoonText": "更多內容即將推出...",
"mostDestroyedPlayerText": "被摧毀次數最多的球員", "mostDestroyedPlayerText": "最遭受擊殺的玩家",
"mostValuablePlayerText": "最有價值的玩家", "mostValuablePlayerText": "最有價值的玩家",
"mostViolatedPlayerText": "最遭受暴力的玩家", "mostViolatedPlayerText": "最遭受暴力的玩家",
"mostViolentPlayerText": "最暴力的玩家", "mostViolentPlayerText": "最暴力的玩家",
@ -1123,7 +1123,7 @@
"okText": "好的", "okText": "好的",
"onText": "開", "onText": "開",
"oneMomentText": "請等待", "oneMomentText": "請等待",
"onslaughtRespawnText": "${PLAYER}將於${WAVE}波復活", "onslaughtRespawnText": "${PLAYER}將於${WAVE}波復活",
"openMeText": "打開我!", "openMeText": "打開我!",
"openNowText": "即時打開", "openNowText": "即時打開",
"openText": "打開", "openText": "打開",
@ -1452,7 +1452,7 @@
"getTokensText": "獲得代幣...", "getTokensText": "獲得代幣...",
"notEnoughTokensText": "代幣不足!", "notEnoughTokensText": "代幣不足!",
"numTokensText": "${COUNT}代幣", "numTokensText": "${COUNT}代幣",
"openNowDescriptionText": "您有足夠的代幣\n現在打開這個 - 你不\n需要等待。", "openNowDescriptionText": "您有足夠的代幣去\n即時打開這寶箱\n毋需等待",
"shinyNewCurrencyText": "炸彈小分隊 閃亮亮 的全新遊戲幣!!", "shinyNewCurrencyText": "炸彈小分隊 閃亮亮 的全新遊戲幣!!",
"tokenPack1Text": "50代幣", "tokenPack1Text": "50代幣",
"tokenPack2Text": "500代幣", "tokenPack2Text": "500代幣",
@ -1616,6 +1616,7 @@
"Arabic": "阿拉伯語", "Arabic": "阿拉伯語",
"Belarussian": "白俄羅斯語", "Belarussian": "白俄羅斯語",
"Chinese": "簡體中文", "Chinese": "簡體中文",
"ChineseSimplified": "簡體中文",
"ChineseTraditional": "繁體中文", "ChineseTraditional": "繁體中文",
"Croatian": "克羅地亞語", "Croatian": "克羅地亞語",
"Czech": "捷克語", "Czech": "捷克語",
@ -1634,17 +1635,22 @@
"Indonesian": "印尼語", "Indonesian": "印尼語",
"Italian": "意大利語", "Italian": "意大利語",
"Japanese": "日語", "Japanese": "日語",
"Kazakh": "哈薩克語",
"Korean": "朝鮮語", "Korean": "朝鮮語",
"Malay": "馬來語", "Malay": "馬來語",
"Persian": "波斯文", "Persian": "波斯文",
"PirateSpeak": "海盜口語", "PirateSpeak": "海盜口語",
"Polish": "波蘭語", "Polish": "波蘭語",
"Portuguese": "葡萄牙語", "Portuguese": "葡萄牙語",
"PortugueseBrazil": "葡萄牙語(巴西)",
"PortuguesePortugal": "葡萄牙語(葡萄牙)",
"Romanian": "羅馬尼亞語", "Romanian": "羅馬尼亞語",
"Russian": "俄羅斯語", "Russian": "俄羅斯語",
"Serbian": "塞爾維亞語", "Serbian": "塞爾維亞語",
"Slovak": "斯洛伐克語", "Slovak": "斯洛伐克語",
"Spanish": "西班牙語", "Spanish": "西班牙語",
"SpanishLatinAmerica": "西班牙語(拉丁美洲)",
"SpanishSpain": "西班牙語(西班牙)",
"Swedish": "瑞典語", "Swedish": "瑞典語",
"Tamil": "泰米爾語", "Tamil": "泰米爾語",
"Thai": "泰語", "Thai": "泰語",
@ -1943,7 +1949,7 @@
"validatingTestBuildText": "測試版驗證中", "validatingTestBuildText": "測試版驗證中",
"viaText": "其他賬戶", "viaText": "其他賬戶",
"victoryText": "勝利!", "victoryText": "勝利!",
"voteDelayText": "你不能在${NUMBER}內發起一個新的投票", "voteDelayText": "你不能在${NUMBER}內發起一個新的投票",
"voteInProgressText": "已經有一個投票正在進行中了", "voteInProgressText": "已經有一個投票正在進行中了",
"votedAlreadyText": "你已經參與過投票了", "votedAlreadyText": "你已經參與過投票了",
"votesNeededText": "通過需要${NUMBER}個投票", "votesNeededText": "通過需要${NUMBER}個投票",

View file

@ -1639,7 +1639,8 @@
"Arabic": "Arabština", "Arabic": "Arabština",
"Belarussian": "Běloruština", "Belarussian": "Běloruština",
"Chinese": "Zjednodušená Čínština", "Chinese": "Zjednodušená Čínština",
"ChineseTraditional": "Tradiční Čínština", "ChineseSimplified": "Čínština - Zjednodušená",
"ChineseTraditional": "Čínština - Tradiční",
"Croatian": "Chorvatština", "Croatian": "Chorvatština",
"Czech": "Čeština", "Czech": "Čeština",
"Danish": "Dánština", "Danish": "Dánština",
@ -1657,17 +1658,22 @@
"Indonesian": "Indonéština", "Indonesian": "Indonéština",
"Italian": "Italština", "Italian": "Italština",
"Japanese": "Japonština", "Japanese": "Japonština",
"Kazakh": "Kazaština",
"Korean": "Korejština", "Korean": "Korejština",
"Malay": "Malajština", "Malay": "Malajština",
"Persian": "Perština", "Persian": "Perština",
"PirateSpeak": "Řeč pirátů", "PirateSpeak": "Řeč pirátů",
"Polish": "Polština", "Polish": "Polština",
"Portuguese": "Portugalština", "Portuguese": "Portugalština",
"PortugueseBrazil": "Portugalština - Brazilská",
"PortuguesePortugal": "Portugalština - Portugalská",
"Romanian": "Rumunština", "Romanian": "Rumunština",
"Russian": "Ruština", "Russian": "Ruština",
"Serbian": "Srbština", "Serbian": "Srbština",
"Slovak": "Slovenština", "Slovak": "Slovenština",
"Spanish": "Španělština", "Spanish": "Španělština",
"SpanishLatinAmerica": "Španělština - Latinsko Americká",
"SpanishSpain": "Španělština - Španělská",
"Swedish": "Švédština", "Swedish": "Švédština",
"Tamil": "Tamiština", "Tamil": "Tamiština",
"Thai": "Thajština", "Thai": "Thajština",

View file

@ -1,50 +1,60 @@
{ {
"accountSettingsWindow": { "accountSettingsWindow": {
"accountNameRules": "Konto navne kan ikke indeholde emojis eller andre specielle bogstaver", "accountNameRules": "Kontonavne kan ikke indeholde emojis eller andre specielle tegn",
"accountProfileText": "(bruger profil)", "accountProfileText": "(bruger profil)",
"accountsText": "Konti", "accountsText": "Konti",
"achievementProgressText": "Achievements: ${COUNT} ud af ${TOTAL}", "achievementProgressText": "Præstationer: ${COUNT} ud af ${TOTAL}",
"campaignProgressText": "Kampagneforløb ${PROGRESS}", "campaignProgressText": "Kampagneforløb ${PROGRESS}",
"changeOncePerSeason": "Du kan kun ændre dette en gang pr. sæson", "changeOncePerSeason": "Du kan kun ændre dette en gang pr. sæson.",
"changeOncePerSeasonError": "Du må vente indtil næste sæson for at ændre dette igen (${NUM} days)", "changeOncePerSeasonError": "Du må vente indtil næste sæson for at ændre dette igen (${NUM} days)",
"customName": "Brugerdefineret Navn", "createAnAccountText": "Opret en konto",
"customName": "Brugernavn",
"deleteAccountText": "Slet konto",
"googlePlayGamesAccountSwitchText": "Hvis du vil bruge en anden Google konto,\nskal du bruge Google Play Games app for at skifte.",
"linkAccountsEnterCodeText": "Indtast kode", "linkAccountsEnterCodeText": "Indtast kode",
"linkAccountsGenerateCodeText": "Generér kode", "linkAccountsGenerateCodeText": "Generér kode",
"linkAccountsInfoText": "(del forløb på tværs af platforme)", "linkAccountsInfoText": "(del fremskridt på tværs af platforme)",
"linkAccountsInstructionsNewText": "For at forbinde to kontoer, generer en kode på den første\nog skriv den kode på den anden. Dataen fra den anden konto\nvil så blive delt mellem dem begge.\n(Dataen fra den første konto vil blive tabt)\n\nDu kan forbinde op til ${COUNT} kontoer.\n\nVIGTIGT: forbind kun kontoer som du ejer;\nHvis du forbinder med din vens konto vil du ikke\nkunne spille online på samme tid som ham.", "linkAccountsInstructionsNewText": "For at forbinde to konti, skal du generere en kode på den første konto\nog skrive koden på den anden konto. Data fra den anden konto\nvil så blive delt mellem dem begge.\n(Data fra den første konto vil blive slettet)\n\nDu kan forbinde op til ${COUNT} konti.\n\nVIGTIGT: Forbind kun konti du ejer;\nHvis du forbinder med din vens konto vil I ikke\nkunne spille online på samme tid.",
"linkAccountsInstructionsText": "For at forbinde to konti, opret en kode på en\naf dem og indtast den kode på den anden.\nFremskridt og inventar vil blive kombineret.\nDu kan forbinde op til ${COUNT} konti.\n\nVIGTIGT: Kun forbinde konti som du ejer!\nHvis du forbinder konti med dine venner\nvil i ikke kunne spille samtidigt!\n\nPlus: dette kan ikke endu fortrydes, så vær forsigtig!", "linkAccountsInstructionsText": "For at forbinde to konti, opret en kode på en\naf dem og indtast den kode på den anden.\nFremskridt og inventar vil blive kombineret.\nDu kan forbinde op til ${COUNT} konti.\n\nVIGTIGT: Kun forbinde konti som du ejer!\nHvis du forbinder konti med dine venner\nvil i ikke kunne spille samtidigt!\n\nPlus: dette kan ikke endu fortrydes, så vær forsigtig!",
"linkAccountsText": "Forbind konti", "linkAccountsText": "Forbind konti",
"linkedAccountsText": "Forbundne konti:", "linkedAccountsText": "Forbundne konti:",
"nameChangeConfirm": "Ændre dit konto navn til ${NAME}?", "manageAccountText": "Administrer konti",
"nameChangeConfirm": "Skift dit kontonavn til ${NAME}?",
"notLoggedInText": "<ikke logget ind>", "notLoggedInText": "<ikke logget ind>",
"resetProgressConfirmNoAchievementsText": "Dette vil nulstille dit co-op forløb og dine\nlokale high-scores (men ikke dine tickets).\nDette kan ikke fortrydes. Er du sikker?", "resetProgressConfirmNoAchievementsText": "Dette vil nulstille dit co-op fremskridt og dine\nlokale highscores (men ikke dine billetter).\nDette kan ikke fortrydes. Er du sikker?",
"resetProgressConfirmText": "Dette vil nulstille dine co-op fremskridt,\nachievements og lokale highschores. \n(men ikke dine tickets). Dette kan ikke \ngøres om. Er du sikker?", "resetProgressConfirmText": "Dette vil nulstille dine co-op fremskridt,\npræstationer og lokale highscores. \n(men ikke dine billetter). Dette kan ikke \ngøres om. Er du sikker?",
"resetProgressText": "Nulstil Process", "resetProgressText": "Nulstil fremskridt",
"setAccountName": "Indstil kontonavn", "setAccountName": "Indstil kontonavn",
"setAccountNameDesc": "Vælg navnet du vil vise til din konto.\nDu kan bruge navnet fra en af dine forbundne\nkontoer eller lave et unikt brugerdefineret navn.", "setAccountNameDesc": "Vælg et navn du vil vise på din konto.\nDu kan bruge navne fra en af dine forbundne\nkonti eller lave et unikt brugernavn.",
"signInInfoText": "Log in for at optjene tickets, konkurrere online \nog dele dit forløb på tværs af enheder.", "signInInfoText": "Log ind for at optjene billetter, konkurrere online \nog dele dit forløb på tværs af enheder.",
"signInText": "Log Ind", "signInText": "Log ind",
"signInWithDeviceInfoText": "(en automatisk oprettet konto kun tilgængelig fra denne enhed)", "signInWithAnEmailAddressText": "Log ind med email",
"signInWithDeviceText": "Log in med en enheds-konto.", "signInWithDeviceInfoText": "(en automatisk oprettet konto er kun tilgængelig fra denne enhed)",
"signInWithDeviceText": "Log ind med en enhedskonto",
"signInWithGameCircleText": "Log in med Game Circle", "signInWithGameCircleText": "Log in med Game Circle",
"signInWithGooglePlayText": "Log in med Google Play", "signInWithGooglePlayText": "Log in med Google Play",
"signInWithTestAccountInfoText": "(ældre kontotype; brug enheds-konti fremover)", "signInWithTestAccountInfoText": "(ældre kontotype; brug enheds-konti fremover)",
"signInWithTestAccountText": "Log in med test konto", "signInWithTestAccountText": "Log in med test konto",
"signInWithText": "Log ind med ${SERVICE}",
"signInWithV2InfoText": "(en konto der virker på alle platforme)",
"signInWithV2Text": "Log ind med en ${APP_NAME} konto",
"signOutText": "Log ud", "signOutText": "Log ud",
"signingInText": "Logger ind...", "signingInText": "Logger ind...",
"signingOutText": "Logger ud...", "signingOutText": "Logger ud...",
"testAccountWarningOculusText": "Advarsel: Du logger ind med en \"test\" bruger. \nDette ville blive erstattet med \"rigtige\" brugere senere i dette \når, hvilket vil tilbyde ticket (billet) købsmuligheder og andre funktioner.\n\nindtilvidere skal du tjene alle dine tickets in-game.\n(Du får dog BombSquad Pro upgrade helt gratis!)", "testAccountWarningOculusText": "Advarsel: Du logger ind med en \"test\" bruger. \nDette ville blive erstattet med \"rigtige\" brugere senere i dette \når, hvilket vil tilbyde ticket (billet) købsmuligheder og andre funktioner.\n\nindtilvidere skal du tjene alle dine tickets in-game.\n(Du får dog BombSquad Pro upgrade helt gratis!)",
"testAccountWarningText": "Advarsel: Du logger nu ind med en \"test\" bruger.\nDenne bruger er bunden med en bestemt enhed og\nkan risikere at blive nulstillet periodisk. (så lad vær\nmed at brug lang tid på at samle/oplåse ting til den)\n\nKør en detailversion af spillet for at bruge en \"rigtig\"\nbruger (Game-Center, Google Plus, osv.) Dette giver\ndig også mulighed for at gemme dine fremskridt i\nskyen og dele det mellem forskellige enheder.", "testAccountWarningText": "Advarsel: Du logger nu ind med en \"test\" bruger.\nDenne bruger er bunden med en bestemt enhed og\nkan risikere at blive nulstillet periodisk. (så lad vær\nmed at brug lang tid på at samle/oplåse ting til den)\n\nKør en detailversion af spillet for at bruge en \"rigtig\"\nbruger (Game-Center, Google Plus, osv.) Dette giver\ndig også mulighed for at gemme dine fremskridt i\nskyen og dele det mellem forskellige enheder.",
"ticketsText": "Tickets (biletter): ${COUNT}", "ticketsText": "Billetter: ${COUNT}",
"titleText": "Din bruger", "titleText": "Konto",
"unlinkAccountsInstructionsText": "Vælg en konto at adskille", "unlinkAccountsInstructionsText": "Vælg den konto du vil adskille",
"unlinkAccountsText": "Adskil kontoer", "unlinkAccountsText": "Adskil konti",
"unlinkLegacyV1AccountsText": "Adskil Legacy (V1) konti",
"v2LinkInstructionsText": "Brug dette link for at oprette en konto eller logge ind.",
"viaAccount": "(via konto ${NAME})", "viaAccount": "(via konto ${NAME})",
"youAreLoggedInAsText": "Du er logget ind som: ", "youAreLoggedInAsText": "Du er logget ind som: ",
"youAreSignedInAsText": "Du er logget ind som:" "youAreSignedInAsText": "Du er logget ind som:"
}, },
"achievementChallengesText": "Achievementudfordringer", "achievementChallengesText": "Præstationsudfordringer",
"achievementText": "Achievement", "achievementText": "Præstation",
"achievements": { "achievements": {
"Boom Goes the Dynamite": { "Boom Goes the Dynamite": {
"description": "Dræb 3 fjender med dynamit", "description": "Dræb 3 fjender med dynamit",
@ -62,8 +72,8 @@
}, },
"Dual Wielding": { "Dual Wielding": {
"descriptionFull": "Forbind 2 kontrollere (hardware eller app)", "descriptionFull": "Forbind 2 kontrollere (hardware eller app)",
"descriptionFullComplete": "2 kontrollere forbundet (hardware eller app)", "descriptionFullComplete": "2 kontrollere er forbundet (hardware eller app)",
"name": "Dobbelt-båren" "name": "Dobbelt våbenføring"
}, },
"Flawless Victory": { "Flawless Victory": {
"description": "Vind uden at blive ramt", "description": "Vind uden at blive ramt",
@ -93,13 +103,13 @@
}, },
"In Control": { "In Control": {
"descriptionFull": "Forbind en controller (hardware eller app)", "descriptionFull": "Forbind en controller (hardware eller app)",
"descriptionFullComplete": "Forbind en controller. (hardware eller app)", "descriptionFullComplete": "En controller forbundet. (hardware eller app)",
"name": "Under Kontrol" "name": "Under kontrol"
}, },
"Last Stand God": { "Last Stand God": {
"description": "Scor 1000 point", "description": "Scor 1000 point",
"descriptionComplete": "Du scorede 1000 point", "descriptionComplete": "Du scorede 1000 point",
"descriptionFull": "Score 1000 point i ${LEVEL}", "descriptionFull": "Scor 1000 point i ${LEVEL}",
"descriptionFullComplete": "Du scorede 1000 point i ${LEVEL}", "descriptionFullComplete": "Du scorede 1000 point i ${LEVEL}",
"name": "Gud i ${LEVEL}" "name": "Gud i ${LEVEL}"
}, },
@ -126,7 +136,7 @@
}, },
"Off You Go Then": { "Off You Go Then": {
"description": "Kast 3 fjender ud af banen", "description": "Kast 3 fjender ud af banen",
"descriptionComplete": "Kastede 3 fjender ud af banen", "descriptionComplete": "Du kastede 3 fjender ud af banen",
"descriptionFull": "Kast 3 fjender af banen i ${LEVEL}", "descriptionFull": "Kast 3 fjender af banen i ${LEVEL}",
"descriptionFullComplete": "Du kastede 3 fjender af banen i ${LEVEL}", "descriptionFullComplete": "Du kastede 3 fjender af banen i ${LEVEL}",
"name": "Afsted med dig" "name": "Afsted med dig"
@ -139,7 +149,7 @@
"name": "Gud i ${LEVEL}" "name": "Gud i ${LEVEL}"
}, },
"Onslaught Master": { "Onslaught Master": {
"description": "Scor 5000 point", "description": "Scor 500 point",
"descriptionComplete": "Du scorede 500 point", "descriptionComplete": "Du scorede 500 point",
"descriptionFull": "Scor 500 point i ${LEVEL}", "descriptionFull": "Scor 500 point i ${LEVEL}",
"descriptionFullComplete": "Du scorede 500 point i ${LEVEL}", "descriptionFullComplete": "Du scorede 500 point i ${LEVEL}",
@ -178,7 +188,7 @@
"descriptionComplete": "Du vandt uden at lade fjenderne score", "descriptionComplete": "Du vandt uden at lade fjenderne score",
"descriptionFull": "Vind ${LEVEL} uden at lade fjenderne score", "descriptionFull": "Vind ${LEVEL} uden at lade fjenderne score",
"descriptionFullComplete": "Du vandt ${LEVEL} uden at lade fjenderne score", "descriptionFullComplete": "Du vandt ${LEVEL} uden at lade fjenderne score",
"name": "Straffespark i ${LEVEL}" "name": "Sejr uden mål i ${LEVEL}"
}, },
"Pro Football Victory": { "Pro Football Victory": {
"description": "Vind kampen", "description": "Vind kampen",
@ -206,7 +216,7 @@
"descriptionComplete": "Du vandt uden at fjenderne scorede", "descriptionComplete": "Du vandt uden at fjenderne scorede",
"descriptionFull": "Vind ${LEVEL} uden at fjenderne scorer", "descriptionFull": "Vind ${LEVEL} uden at fjenderne scorer",
"descriptionFullComplete": "Du vandt ${LEVEL} uden at fjenderne scorede", "descriptionFullComplete": "Du vandt ${LEVEL} uden at fjenderne scorede",
"name": "Straffespark i ${LEVEL}" "name": "Sejr uden mål i ${LEVEL}"
}, },
"Rookie Football Victory": { "Rookie Football Victory": {
"description": "Vind kampen", "description": "Vind kampen",
@ -246,7 +256,7 @@
"Sharing is Caring": { "Sharing is Caring": {
"descriptionFull": "Del spillet med en ven", "descriptionFull": "Del spillet med en ven",
"descriptionFullComplete": "Har delt spillet med en ven", "descriptionFullComplete": "Har delt spillet med en ven",
"name": "Sharing is Caring" "name": "Deling er heling"
}, },
"Stayin' Alive": { "Stayin' Alive": {
"description": "Vind uden at dø", "description": "Vind uden at dø",
@ -267,40 +277,40 @@
"descriptionComplete": "Du gav 50% skade med ét slag", "descriptionComplete": "Du gav 50% skade med ét slag",
"descriptionFull": "Giv 50% skade med ét slag i ${LEVEL}", "descriptionFull": "Giv 50% skade med ét slag i ${LEVEL}",
"descriptionFullComplete": "Du gav 50% skade med ét slag i ${LEVEL}", "descriptionFullComplete": "Du gav 50% skade med ét slag i ${LEVEL}",
"name": "Super slag" "name": "Super Slag"
}, },
"TNT Terror": { "TNT Terror": {
"description": "Dræb 6 fjender med TNT", "description": "Dræb 6 fjender med TNT",
"descriptionComplete": "Du dræbte 6 fjender med TNT", "descriptionComplete": "Du dræbte 6 fjender med TNT",
"descriptionFull": "Dræb 6 fjender med TNT i ${LEVEL}", "descriptionFull": "Dræb 6 fjender med TNT i ${LEVEL}",
"descriptionFullComplete": "Du dræbte 6 fjender med TNT i ${LEVEL}", "descriptionFullComplete": "Du dræbte 6 fjender med TNT i ${LEVEL}",
"name": "TNT-terror" "name": "TNT Terror"
}, },
"Team Player": { "Team Player": {
"descriptionFull": "Start et Hold spil med mere end 4 spillere", "descriptionFull": "Start et holdspil med mere end 4 spillere",
"descriptionFullComplete": "Startede et Hold spil med mere end 4 spillere", "descriptionFullComplete": "Startede et holdspil med mere end 4 spillere",
"name": "Hold Spiller" "name": "Holdspiller"
}, },
"The Great Wall": { "The Great Wall": {
"description": "Stop hver eneste fjende", "description": "Stop alle fjender",
"descriptionComplete": "Du stoppede hver eneste fjende", "descriptionComplete": "Du stoppede alle fjender",
"descriptionFull": "Stop hver eneste fjende i ${LEVEL}", "descriptionFull": "Stop alle fjender i ${LEVEL}",
"descriptionFullComplete": "Du stoppede hver eneste fjende i ${LEVEL}", "descriptionFullComplete": "Du stoppede alle fjender i ${LEVEL}",
"name": "Den Store Mur" "name": "Den Store Mur"
}, },
"The Wall": { "The Wall": {
"description": "Stop hver eneste fjende", "description": "Stop alle fjender",
"descriptionComplete": "Du stoppede hver eneste fjende", "descriptionComplete": "Du stoppede alle fjender",
"descriptionFull": "Stop hver eneste fjende i ${LEVEL}", "descriptionFull": "Stop alle fjender i ${LEVEL}",
"descriptionFullComplete": "Du stoppede hver eneste fjende i ${LEVEL}", "descriptionFullComplete": "Du stoppede alle fjender i ${LEVEL}",
"name": "Muren" "name": "Muren"
}, },
"Uber Football Shutout": { "Uber Football Shutout": {
"description": "Vind uden at fjenderne scorer", "description": "Vind uden at fjenden scorer",
"descriptionComplete": "Du vandt uden at fjenden scorede", "descriptionComplete": "Du vandt uden at fjenden scorede",
"descriptionFull": "Vind ${LEVEL} uden at fjenden scorer", "descriptionFull": "Vind ${LEVEL} uden at fjenden scorer",
"descriptionFullComplete": "Du vandt ${LEVEL} uden at fjenden scorede", "descriptionFullComplete": "Du vandt ${LEVEL} uden at fjenden scorede",
"name": "Straffespark i ${LEVEL}" "name": "Sejr uden mål i ${LEVEL}"
}, },
"Uber Football Victory": { "Uber Football Victory": {
"description": "Vind kampen", "description": "Vind kampen",
@ -324,20 +334,26 @@
"name": "Sejr i ${LEVEL}" "name": "Sejr i ${LEVEL}"
} }
}, },
"achievementsRemainingText": "Achievements tilbage:", "achievementsRemainingText": "Præstationer tilbage:",
"achievementsText": "Achievements", "achievementsText": "Præstationer",
"achievementsUnavailableForOldSeasonsText": "Beklager, det er ikke muligt at se detaljer for tidligere sæsoner.", "achievementsUnavailableForOldSeasonsText": "Beklager, det er ikke muligt at se præstationer for tidligere sæsoner.",
"activatedText": "${THING} aktiveret.",
"addGameWindow": { "addGameWindow": {
"getMoreGamesText": "Få Flere Spil...", "getMoreGamesText": "Få flere spil...",
"titleText": "Tilføj spil" "titleText": "Tilføj spil"
}, },
"addToFavoritesText": "Tilføj til favoritter",
"addedToFavoritesText": "Tilføjede '${NAME}' to favoritter.",
"allText": "Alt",
"allowText": "Tillad", "allowText": "Tillad",
"alreadySignedInText": "Din konto er logget ind fra en anden enhed;\nvær sød at skifte konto eller luk spillet ned på\ndine andre enheder og prøv igen", "alreadySignedInText": "Din konto er logget ind på en anden enhed;\nskift konto eller luk spillet på\ndine andre enheder og prøv igen.",
"apiVersionErrorText": "Kan ikke indlæse ${NAME}; det er målrettet api-version ${VERSION_USED}; vi behøver ${VERSION_REQUIRED}.", "apiVersionErrorText": "Kan ikke indlæse modulet ${NAME}; det er målrettet api-version ${VERSION_USED}; vi kræver ${VERSION_REQUIRED}.",
"applyText": "Tilføj",
"areYouSureText": "Er du sikker?",
"audioSettingsWindow": { "audioSettingsWindow": {
"headRelativeVRAudioInfoText": "(Sætter kun dette til automatisk når høretelefoner er sat i)", "headRelativeVRAudioInfoText": "(Er kun automatisk når høretelefoner er sat i)",
"headRelativeVRAudioText": "Head-Relative VR Audio", "headRelativeVRAudioText": "Head-Relative VR Audio",
"musicVolumeText": "Musiklydstyrke", "musicVolumeText": "Lydstyrke musik",
"soundVolumeText": "Lydstyrke", "soundVolumeText": "Lydstyrke",
"soundtrackButtonText": "Lydspor", "soundtrackButtonText": "Lydspor",
"soundtrackDescriptionText": "(Afspil din egen musik, mens du spiller)", "soundtrackDescriptionText": "(Afspil din egen musik, mens du spiller)",
@ -345,11 +361,11 @@
}, },
"autoText": "Auto", "autoText": "Auto",
"backText": "Tilbage", "backText": "Tilbage",
"banThisPlayerText": "Ban denne spiller", "banThisPlayerText": "Bloker denne spiller",
"bestOfFinalText": "Bedst ud af ${COUNT} finale", "bestOfFinalText": "Bedst ud af ${COUNT} finale",
"bestOfSeriesText": "Bedst ud af ${COUNT}:", "bestOfSeriesText": "Bedst ud af ${COUNT}:",
"bestRankText": "Dine bedste er #${RANK}", "bestRankText": "Din bedste er #${RANK}",
"bestRatingText": "Din bedste vurdering er ${RATING}", "bestRatingText": "Din bedste bedømmelse er ${RATING}",
"betaErrorText": "Denne beta er ikke længere aktiv; tjek venligst om der er en ny version tilgængelig", "betaErrorText": "Denne beta er ikke længere aktiv; tjek venligst om der er en ny version tilgængelig",
"betaValidateErrorText": "Ude af stand til til at validere beta. Har du tjekket din internetforbindelse?", "betaValidateErrorText": "Ude af stand til til at validere beta. Har du tjekket din internetforbindelse?",
"betaValidatedText": "Beta valideret; Nyd den!", "betaValidatedText": "Beta valideret; Nyd den!",
@ -358,15 +374,25 @@
"boostText": "Boost", "boostText": "Boost",
"bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} konfigureres i selve app'en.", "bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} konfigureres i selve app'en.",
"buttonText": "Knap", "buttonText": "Knap",
"canWeDebugText": "Er det okay at BombSquad automatisk rapporterer fejl, \ncrashes og generelt brug til udvikleren?\n\nDenne data indeholder ingen personlige informationer og hjælper med \nat få spillet til at kører flydende og uden fejl.", "canWeDebugText": "Må ${APP_NAME} automatisk rapportere fejl, \nnedbrud og generel brug til udvikleren?\n\nDenne data indeholder ingen personlige informationer og hjælper\nmed at få spillet til at kører flydende og uden fejl.",
"cancelText": "Annuller", "cancelText": "Annuller",
"cantConfigureDeviceText": "Undskyld, ${DEVICE} kan ikke konfigureres.", "cantConfigureDeviceText": "${DEVICE} kan ikke konfigureres.",
"challengeEndedText": "Udfordringen er afsluttet.", "challengeEndedText": "Denne udfordring er afsluttet.",
"chatMuteText": "Slå chat fra", "chatMuteText": "Slå chat fra",
"chatMutedText": "Chat fra", "chatMutedText": "Chat fra",
"chatUnMuteText": "Slå chat til", "chatUnMuteText": "Slå chat til",
"chests": {
"prizeOddsText": "Vinderchancer",
"reduceWaitText": "Reducer ventetid",
"slotDescriptionText": "Denne plads kan indeholde en kiste.\n\nVind kister ved at spille kampagnelevels,\nfå placeringer i turneringer, og ved at løse\npræstationer.",
"slotText": "Kisteplads ${NUM}",
"slotsFullWarningText": "Advarsel: Alle dine pladser til kister er optaget.\nKister du vinder i dette spil vil gå tabt.",
"unlocksInText": "Oplåses om"
},
"choosingPlayerText": "<vælger spiller>", "choosingPlayerText": "<vælger spiller>",
"completeThisLevelToProceedText": "Du bliver nødt til at gennemføre\ndette level for at fortsætte!", "claimText": "Indløs",
"codesExplainText": "Koder leveres af udvikleren for\nat diagnosticere og tilrette kontoproblemer.",
"completeThisLevelToProceedText": "Du skal gennemføre\ndette niveau for at fortsætte!",
"completionBonusText": "Gennemføringsbonus", "completionBonusText": "Gennemføringsbonus",
"configControllersWindow": { "configControllersWindow": {
"configureControllersText": "Konfigurer Controllers", "configureControllersText": "Konfigurer Controllers",
@ -378,12 +404,12 @@
"ps3Text": "PS3-controllere", "ps3Text": "PS3-controllere",
"titleText": "Controllere", "titleText": "Controllere",
"wiimotesText": "Wiimotes", "wiimotesText": "Wiimotes",
"xbox360Text": "Xbox 360-controllere" "xbox360Text": "Xbox 360 controllere"
}, },
"configGamepadSelectWindow": { "configGamepadSelectWindow": {
"androidNoteText": "Bemærk: Controllersupport varierer fra enhed til enhed og Android-version.", "androidNoteText": "Bemærk: Controllersupport varierer fra enhed til enhed og Android-version.",
"pressAnyButtonText": "Tryk på en hvilkårlig knap på controller\nsom du gerne vil konfigurere.", "pressAnyButtonText": "Tryk på en vilkårlig knap på controlleren\nsom du gerne vil konfigurere...",
"titleText": "Konfigurér Controllers" "titleText": "Konfigurér Controllere"
}, },
"configGamepadWindow": { "configGamepadWindow": {
"advancedText": "Avanceret", "advancedText": "Avanceret",
@ -417,7 +443,7 @@
"runTrigger1Text": "Løbeudløser 1", "runTrigger1Text": "Løbeudløser 1",
"runTrigger2Text": "Løbeudløser 2", "runTrigger2Text": "Løbeudløser 2",
"runTriggerDescriptionText": "(analoge udløserere lader dig løbe i forskellige hastigheder)", "runTriggerDescriptionText": "(analoge udløserere lader dig løbe i forskellige hastigheder)",
"secondHalfText": "Brug denne til at konfigurere den anden\ndel af en 2-i-1-enhed, som\nviser sig som en enkelt gamepad.", "secondHalfText": "Brug denne til at konfigurere den anden\ndel af en 2-i-1-enhed, som\nviser sig som en enkelt controller.",
"secondaryEnableText": "Aktivér", "secondaryEnableText": "Aktivér",
"secondaryText": "Sekundær Controller", "secondaryText": "Sekundær Controller",
"startButtonActivatesDefaultDescriptionText": "(sluk denne, hvis din 'start'-knap fungerer mere som en 'menu'-knap)", "startButtonActivatesDefaultDescriptionText": "(sluk denne, hvis din 'start'-knap fungerer mere som en 'menu'-knap)",
@ -426,14 +452,14 @@
"twoInOneSetupText": "2-i-1 controllersetup", "twoInOneSetupText": "2-i-1 controllersetup",
"uiOnlyDescriptionText": "(forhindre, at denne controller deltager i et spil)", "uiOnlyDescriptionText": "(forhindre, at denne controller deltager i et spil)",
"uiOnlyText": "Begræns til Menu Brug", "uiOnlyText": "Begræns til Menu Brug",
"unassignedButtonsRunText": "Alle knapper, der ikke er i brug, bruges som løbeknap", "unassignedButtonsRunText": "Alle knapper, der ikke er i brug, er løbeknapper",
"unsetText": "<utildelt>", "unsetText": "<utildelt>",
"vrReorientButtonText": "VR Positionsnulstillings Knap" "vrReorientButtonText": "VR Positionsnulstillings Knap"
}, },
"configKeyboardWindow": { "configKeyboardWindow": {
"configuringText": "Konfigurerer ${DEVICE}", "configuringText": "Konfigurerer ${DEVICE}",
"keyboard2NoteScale": 0.7, "keyboard2NoteScale": 0.7,
"keyboard2NoteText": "Bemærk: De fleste tastaturer kan kun registrere nogle få tryk\nad gangen, så det er en god idé at have et ekstra\ntastatur. Bemærk, at du stadig skal registrere unikke\nknapfunktioner til de to spillere." "keyboard2NoteText": "Bemærk: De fleste tastaturer kan kun registrere få tryk\nad gangen, så for to tastaturspillere er det er en god idé\nat have et ekstra tastatur forbundet.\nBemærk, at du her stadig skal registrere unikke knapper\ntil de to spillere."
}, },
"configTouchscreenWindow": { "configTouchscreenWindow": {
"actionControlScaleText": "Action kontrol vægt", "actionControlScaleText": "Action kontrol vægt",
@ -444,19 +470,20 @@
"movementControlScaleText": "Bevægelseskontrol vægt", "movementControlScaleText": "Bevægelseskontrol vægt",
"movementText": "Bevægelse", "movementText": "Bevægelse",
"resetText": "Nulstil", "resetText": "Nulstil",
"swipeControlsHiddenText": "Gem 'swipe' ikoner.", "swipeControlsHiddenText": "Gem 'swipe' ikoner",
"swipeInfoText": "'Swipe'-kontrol tager lidt tid at vænne sig til, men\ndet gør det lettere at spille uden at kigge på knapperne.", "swipeInfoText": "'Swipe'-kontrol tager lidt tid at vænne sig til, men\ndet gør det lettere at spille uden at kigge på knapperne.",
"swipeText": "swipe", "swipeText": "swipe",
"titleText": "Konfigurér touchskærm", "titleText": "Konfigurér touchskærm",
"touchControlsScaleText": "Skala ift. berøringsstyring" "touchControlsScaleText": "Skala ift. berøringsstyring"
}, },
"configureDeviceInSystemSettingsText": "${DEVICE} kan konfigureres i System Indstillings app'en.",
"configureItNowText": "Konfigurér den nu?", "configureItNowText": "Konfigurér den nu?",
"configureText": "Konfigurér", "configureText": "Konfigurér",
"connectMobileDevicesWindow": { "connectMobileDevicesWindow": {
"amazonText": "Amazon Appstore", "amazonText": "Amazon Appstore",
"appStoreText": "App Store", "appStoreText": "App Store",
"bestResultsScale": 0.65, "bestResultsScale": 0.65,
"bestResultsText": "For at få de bedste resultater skal du bruge et lag-frit wifi-netværk.\nDu kan reducere wifi-lag ved at slukke for andre trådløse enheder,\nved at spille tæt ved din wifi-router og ved at forbinde spilværten\ndirekte til netværket via internet.", "bestResultsText": "For at få de bedste resultater skal du bruge et lag-frit wifi-netværk.\nDu kan reducere wifi-lag ved at slukke for andre trådløse enheder,\nved at spille tæt ved din wifi-router og ved at forbinde spilværten\ndirekte til netværket med et kabel.",
"explanationScale": 0.8, "explanationScale": 0.8,
"explanationText": "For at bruge en smartphone eller tablet som en trådløs controller,\nskal du installere app'en \"${REMOTE_APP_NAME}\". Et utal af enheder\nkan forbinde til et ${APP_NAME} spil via WiFi, og det er helt gratis!", "explanationText": "For at bruge en smartphone eller tablet som en trådløs controller,\nskal du installere app'en \"${REMOTE_APP_NAME}\". Et utal af enheder\nkan forbinde til et ${APP_NAME} spil via WiFi, og det er helt gratis!",
"forAndroidText": "til Android:", "forAndroidText": "til Android:",
@ -469,11 +496,11 @@
"continueText": "Fortsæt", "continueText": "Fortsæt",
"controlsText": "Styring", "controlsText": "Styring",
"coopSelectWindow": { "coopSelectWindow": {
"activenessAllTimeInfoText": "Nem mindenki Joshua", "activenessAllTimeInfoText": "Dette medregnes ikke ved all-time rangering.",
"activenessInfoText": "Denne multiplikator siger på dage hvor du\nspiller og falder på dage hvor du ikke spiller.", "activenessInfoText": "Denne multiplikator stiger på dage hvor du\nspiller og falder på dage hvor du ikke spiller.",
"activityText": "Aktivitet", "activityText": "Aktivitet",
"campaignText": "Kampagne", "campaignText": "Kampagne",
"challengesInfoText": "Tjen præmier ved at gennemføre mini-games.\n\nPræmier og vanskelighedniveauer stiger \nhver gang en udfordring er gennemført og\nfalder når én udløber eller er annuleret.", "challengesInfoText": "Tjen præmier ved at gennemføre mini-games.\n\nPræmier og vanskelighedsniveauer stiger \nhver gang en udfordring er gennemført og\nfalder når én udløber eller opgives.",
"challengesText": "Udfordringer", "challengesText": "Udfordringer",
"currentBestText": "Nuværende bedste", "currentBestText": "Nuværende bedste",
"customText": "Brugerdefineret", "customText": "Brugerdefineret",
@ -495,23 +522,24 @@
"powerRankingPointsToRankedText": "(${CURRENT} ud af ${REMAINING} point)", "powerRankingPointsToRankedText": "(${CURRENT} ud af ${REMAINING} point)",
"powerRankingText": "Power Rangering", "powerRankingText": "Power Rangering",
"prizesText": "Priser", "prizesText": "Priser",
"proMultInfoText": "Spillere med en ${PRO} opgradering\nfår ${PERCENT}% point boost her", "proMultInfoText": "Spillere med en ${PRO} opgradering\nfår ${PERCENT}% point boost her.",
"seeMoreText": "Flere...", "seeMoreText": "Flere...",
"skipWaitText": "Skip at vente", "skipWaitText": "Skip at vente",
"timeRemainingText": "Tid tilbage", "timeRemainingText": "Tid tilbage",
"titleText": "Co-op", "titleText": "Co-op",
"toRankedText": "Til rangeret", "toRankedText": "Til rangeret",
"totalText": "i alt", "totalText": "i alt",
"tournamentInfoText": "Konkurrer efter high scores med\nandre spillere i din liga.\n\nPræmier er tildelt til de top scorende\nspillere når turneringens tid er udløbet.", "tournamentInfoText": "Konkurrer efter highscores med\nandre spillere i din liga.\n\nPræmier er tildelt til de top scorende\nspillere når turneringens tid er udløbet.",
"welcome1Text": "Velkommen til ${LEAGUE}. Du kan forbedre din\nliga rang ved at indtjene stjerne bedømmelser, klare\npræstationer og vinde trofæer i turneringer.", "welcome1Text": "Velkommen til ${LEAGUE}. Du kan forbedre din\nliga rang ved at indtjene stjerne bedømmelser, klare\npræstationer og vinde trofæer i turneringer.",
"welcome2Text": "Du kan også vinde billetter fra mange af de samme aktiviteter.\nBilletter kan blive brugt til oplåsning af nye spilkarakterer, baner, og\nmini-games, at komme ind i turneringer, og meget mere.", "welcome2Text": "Du kan også vinde billetter fra mange af de samme aktiviteter.\nBilletter kan blive brugt til at oplåse nye karakterer, baner,\nmini-games, turneringer og meget mere.",
"yourPowerRankingText": "Din Power Rangering" "yourPowerRankingText": "Din Power Rangering:"
}, },
"copyConfirmText": "Kopier til udklipsholder.",
"copyOfText": "${NAME} kopi", "copyOfText": "${NAME} kopi",
"copyText": "Kopier", "copyText": "Kopier",
"copyrightText": "© 2013 Eric Froemling", "copyrightText": "© 2013 Eric Froemling",
"createAPlayerProfileText": "Opret en spillerprofil?", "createAPlayerProfileText": "Opret en spillerprofil?",
"createEditPlayerText": "<Lav/Rediger Spiller>", "createEditPlayerText": "<Opret/Rediger Spiller>",
"createText": "Opret", "createText": "Opret",
"creditsWindow": { "creditsWindow": {
"additionalAudioArtIdeasText": "Ekstra lyd, tidlige illustrationer og idéer af ${NAME}", "additionalAudioArtIdeasText": "Ekstra lyd, tidlige illustrationer og idéer af ${NAME}",
@ -536,7 +564,7 @@
"deathsText": "Antal gange død", "deathsText": "Antal gange død",
"debugText": "debug", "debugText": "debug",
"debugWindow": { "debugWindow": {
"reloadBenchmarkBestResultsText": "Obs: Det anbefales at du sætter Indstillinger->Grafik-> Teksturer til 'høj' når du tester dette.", "reloadBenchmarkBestResultsText": "Obs: Det anbefales at du sætter Indstillinger->Grafik->Teksturer til 'høj' når du tester dette.",
"runCPUBenchmarkText": "Kør CPU Benchmark", "runCPUBenchmarkText": "Kør CPU Benchmark",
"runGPUBenchmarkText": "Kør GPU Benchmark", "runGPUBenchmarkText": "Kør GPU Benchmark",
"runMediaReloadBenchmarkText": "Kør Media-Reload Benchmark", "runMediaReloadBenchmarkText": "Kør Media-Reload Benchmark",
@ -552,7 +580,7 @@
"unlockCoopText": "Åbn co-op levels" "unlockCoopText": "Åbn co-op levels"
}, },
"defaultFreeForAllGameListNameText": "Standard alle mod alle-spil", "defaultFreeForAllGameListNameText": "Standard alle mod alle-spil",
"defaultGameListNameText": "Standard ${PLAYMODE} spilleliste", "defaultGameListNameText": "Standard ${PLAYMODE} Playliste",
"defaultNewFreeForAllGameListNameText": "Mine alle mod alle-spil", "defaultNewFreeForAllGameListNameText": "Mine alle mod alle-spil",
"defaultNewGameListNameText": "Min ${PLAYMODE} playliste", "defaultNewGameListNameText": "Min ${PLAYMODE} playliste",
"defaultNewTeamGameListNameText": "Mine holdspil", "defaultNewTeamGameListNameText": "Mine holdspil",
@ -560,20 +588,27 @@
"deleteText": "Slet", "deleteText": "Slet",
"demoText": "Demo", "demoText": "Demo",
"denyText": "Afvis", "denyText": "Afvis",
"desktopResText": "Computeropløsning", "deprecatedText": "Udfaset",
"descriptionText": "Beskrivelse",
"desktopResText": "Skærmopløsning",
"deviceAccountUpgradeText": "Advarsel:\nDu er logget ind med enhedskontoen (${NAME}).\nEnhedskonti bliver fjernet i en kommende opdatering.\nOpgrader til en V2 konto, hvis du vil beholde dine fremskridt.",
"difficultyEasyText": "Nemt", "difficultyEasyText": "Nemt",
"difficultyHardOnlyText": "Svær niveautilstand udelukkende", "difficultyHardOnlyText": "Kun svært niveau",
"difficultyHardText": "Svær", "difficultyHardText": "Svært",
"difficultyHardUnlockOnlyText": "Dette niveau kan kun blive oplåst på svært niveautilstand.\nTror du at du har hvad der skal til!?!?!?", "difficultyHardUnlockOnlyText": "Dette niveau kan kun blive oplåst på svært niveau.\nTror du, at du har, hvad der skal til!?!?!",
"directBrowserToURLText": "Henvis venligst en web-browser til den følgende URL:", "directBrowserToURLText": "Henvis venligst en web-browser til den følgende URL:",
"disableRemoteAppConnectionsText": "Deaktiver Remote-App forbindelser", "disableRemoteAppConnectionsText": "Deaktiver Remote-App forbindelser",
"disableXInputDescriptionText": "Tillader mere end 4 kontrollere men måske virker det ikke", "disableXInputDescriptionText": "Tillader mere end 4 kontrollere, men det virker måske ikke.",
"disableXInputText": "Deaktiver XInput", "disableXInputText": "Deaktiver XInput",
"disabledText": "Deaktiveret",
"discardText": "Kassér",
"discordFriendsText": "Søger du efter nye spillere at spille med?\nTilmeld dig til vores Discord og find nye venner!",
"discordJoinText": "Tilmeld Discord",
"doneText": "Færdig", "doneText": "Færdig",
"drawText": "Uafgjort", "drawText": "Uafgjort",
"duplicateText": "Dupliker", "duplicateText": "Kopier",
"editGameListWindow": { "editGameListWindow": {
"addGameText": "Tilføj spil", "addGameText": "Tilføj\nspil",
"cantOverwriteDefaultText": "Kan ikke overskrive standard spilliste!", "cantOverwriteDefaultText": "Kan ikke overskrive standard spilliste!",
"cantSaveAlreadyExistsText": "En spilleliste med det navn eksisterer allerede!", "cantSaveAlreadyExistsText": "En spilleliste med det navn eksisterer allerede!",
"cantSaveEmptyListText": "En tom spilleliste kan ikke gemmes!", "cantSaveEmptyListText": "En tom spilleliste kan ikke gemmes!",
@ -586,12 +621,12 @@
"titleText": "Spilleliste editor" "titleText": "Spilleliste editor"
}, },
"editProfileWindow": { "editProfileWindow": {
"accountProfileInfoText": "Denne specielle profil har et navn\nog et ikon baseret på din konto\n\n${ICONS}\n\nLav brugdefinerede profiler for at bruge\nforskellige navne og brugerdefinerede ikoner.", "accountProfileInfoText": "Denne specielle profil har et navn\nog et ikon baseret på din konto\n\n${ICONS}\n\nLav brugerprofiler for at bruge\nforskellige navne og brugerdefinerede ikoner.",
"accountProfileText": "(konto profil)", "accountProfileText": "(konto profil)",
"availableText": "Dette navn \"${NAME}\" er ledigt.", "availableText": "Dette navn \"${NAME}\" er ledigt.",
"changesNotAffectText": "Bemærk: ændringer vil ikke have nogen indvirkning på spillere, som allerede er i spil.", "changesNotAffectText": "Bemærk: ændringer vil ikke have nogen indvirkning på spillere, som allerede er i spil.",
"characterText": "karakter", "characterText": "karakter",
"checkingAvailabilityText": "Checker muligheden for \"${NAME}\"...", "checkingAvailabilityText": "Undersøger muligheden for \"${NAME}\"...",
"colorText": "farve", "colorText": "farve",
"getMoreCharactersText": "Få flere karakterer...", "getMoreCharactersText": "Få flere karakterer...",
"getMoreIconsText": "Få flere ikoner...", "getMoreIconsText": "Få flere ikoner...",
@ -603,6 +638,7 @@
"localProfileText": "(lokal profil)", "localProfileText": "(lokal profil)",
"nameDescriptionText": "Spillernavn", "nameDescriptionText": "Spillernavn",
"nameText": "Navn", "nameText": "Navn",
"profileAlreadyExistsText": "En profil med det navn eksisterer allerede.",
"randomText": "tilfældig", "randomText": "tilfældig",
"titleEditText": "Rediger profil", "titleEditText": "Rediger profil",
"titleNewText": "Ny profil", "titleNewText": "Ny profil",
@ -624,7 +660,7 @@
"duplicateText": "Dupliker\nLydspor", "duplicateText": "Dupliker\nLydspor",
"editSoundtrackText": "Lydspor Editor", "editSoundtrackText": "Lydspor Editor",
"editText": "Rediger\nLydspor", "editText": "Rediger\nLydspor",
"fetchingITunesText": "Henter iTunes spilleliste...", "fetchingITunesText": "henter Musik App spilleliste...",
"musicVolumeZeroWarning": "Advarsel: Din lydstyrke i spillet er sat til 0", "musicVolumeZeroWarning": "Advarsel: Din lydstyrke i spillet er sat til 0",
"nameText": "Navn", "nameText": "Navn",
"newSoundtrackNameText": "Mit lydspor ${COUNT}", "newSoundtrackNameText": "Mit lydspor ${COUNT}",
@ -636,57 +672,65 @@
"testText": "Hør", "testText": "Hør",
"titleText": "Lydspor", "titleText": "Lydspor",
"useDefaultGameMusicText": "Standard spilmusik", "useDefaultGameMusicText": "Standard spilmusik",
"useITunesPlaylistText": "iTunesplayliste", "useITunesPlaylistText": "Musik App Playliste",
"useMusicFileText": "Musikfil (mp3 osv)", "useMusicFileText": "Musikfil (mp3 osv)",
"useMusicFolderText": "Mappe med musikfiler" "useMusicFolderText": "Mappe med musikfiler"
}, },
"editText": "Rediger", "editText": "Rediger",
"enabledText": "Aktiveret",
"endText": "Afslut", "endText": "Afslut",
"enjoyText": "God fornøjelse!", "enjoyText": "God fornøjelse!",
"epicDescriptionFilterText": "${DESCRIPTION} i imponerende slowmotion.", "epicDescriptionFilterText": "${DESCRIPTION} i imponerende slowmotion.",
"epicNameFilterText": "Imponerende ${NAME}", "epicNameFilterText": "Imponerende ${NAME}",
"errorAccessDeniedText": "Adgang nægtet", "errorAccessDeniedText": "Adgang nægtet",
"errorDeviceTimeIncorrectText": "Din enheds tid er ikke korrekt med ${HOURS} timer.\nDet vil formentlig skabe problemer.\nTjek din tid- og tidszone indstillinger.",
"errorOutOfDiskSpaceText": "ikke nok disk plads", "errorOutOfDiskSpaceText": "ikke nok disk plads",
"errorSecureConnectionFailText": "Det er ikke muligt at lave en sikker cloud forbindelse; netværksfunktionalitet vil måske fejle.",
"errorText": "Fejl", "errorText": "Fejl",
"errorUnknownText": "ukendt fejl", "errorUnknownText": "ukendt fejl",
"exitGameText": "Afslut ${APP_NAME}?", "exitGameText": "Afslut ${APP_NAME}?",
"expiredAgoText": "Udløb for ${T} siden",
"expiresInText": "Udløber om ${T}",
"exportSuccessText": "'${NAME}' eksporteret.", "exportSuccessText": "'${NAME}' eksporteret.",
"externalStorageText": "Ekstern lagring", "externalStorageText": "Ekstern lagring",
"failText": "Du tabte", "failText": "Du tabte",
"fatalErrorText": "Uh åh; noget mangler eller er i stykker.\nVær venlig og geninstaller appen eller\nkontakt ${EMAIL} for hjælp.", "fatalErrorText": "Åh nej; noget mangler eller er i stykker.\nVenligst geninstaller appen eller\nkontakt ${EMAIL} for hjælp.",
"fileSelectorWindow": { "fileSelectorWindow": {
"titleFileFolderText": "Vælg en fil eller mappe", "titleFileFolderText": "Vælg en fil eller mappe",
"titleFileText": "Vælg en fil", "titleFileText": "Vælg en fil",
"titleFolderText": "Vælg en mappe", "titleFolderText": "Vælg en mappe",
"useThisFolderButtonText": "Brug denne mappe" "useThisFolderButtonText": "Brug denne mappe"
}, },
"filterText": "Filtrer",
"finalScoreText": "Endelig score", "finalScoreText": "Endelig score",
"finalScoresText": "Endelige scorer", "finalScoresText": "Endelige scorer",
"finalTimeText": "Endelig tid", "finalTimeText": "Endelig tid",
"finishingInstallText": "Afslutter installationen; et øjeblik...", "finishingInstallText": "Afslutter installationen; et øjeblik...",
"fireTVRemoteWarningText": "* For at få den bedste oplevelse, brug \nspilkontrollere eller installer\n'${REMOTE_APP_NAME}' appen på din \ntelefon eller tablet.", "fireTVRemoteWarningText": "* For at få den bedste oplevelse, brug \nspilkontrollere eller installer\n'${REMOTE_APP_NAME}' appen på din \ntelefon eller tablet.",
"firstToFinalText": "Først til ${COUNT} Finale", "firstToFinalText": "Første-til-${COUNT} Finale",
"firstToSeriesText": "Første til ${COUNT}", "firstToSeriesText": "Første-til-${COUNT} serie",
"fiveKillText": "FEM DRAB!!!!", "fiveKillText": "FEM DRAB!!!",
"flawlessWaveText": "Fejlfri bølge!", "flawlessWaveText": "Fejlfri bølge!",
"fourKillText": "FIREDOBBELT DRAB!!!!", "fourKillText": "FIREDOBBELT DRAB!!!",
"freeForAllText": "Alle mod alle", "freeForAllText": "Alle mod alle",
"friendScoresUnavailableText": "Dine venners scorer er utilgængelige.", "friendScoresUnavailableText": "Dine venners scorer er utilgængelige.",
"gameCenterText": "GameCenter", "gameCenterText": "GameCenter",
"gameCircleText": "GameCircle", "gameCircleText": "GameCircle",
"gameLeadersText": "Stilling efter ${COUNT}. spil:", "gameLeadersText": "Stilling efter ${COUNT} spil",
"gameListWindow": { "gameListWindow": {
"cantDeleteDefaultText": "Du kan ikke slette standard spilliste!", "cantDeleteDefaultText": "Du kan ikke slette standard spillisten.",
"cantEditDefaultText": "Kan ikke redigere i standardspillisten! Dupliker den eller lav en ny.", "cantEditDefaultText": "Kan ikke redigere i standard spillisten! Kopier den eller opret en ny.",
"cantShareDefaultText": "Du kan ikke dele den standard spilliste.", "cantShareDefaultText": "Du kan ikke dele standard spillisten.",
"deleteConfirmText": "Slet \"${LIST}\"?", "deleteConfirmText": "Slet \"${LIST}\"?",
"deleteText": "Slet\nSpilliste", "deleteText": "Slet\nSpilliste",
"duplicateText": "Dupliker\nSpilliste", "duplicateText": "Dupliker\nSpilliste",
"editText": "Rediger\nSpilliste", "editText": "Rediger\nSpilliste",
"gameListText": "Spilliste", "gameListText": "Spilliste",
"newText": "Ny\nSpilliste", "newText": "Ny\nSpilliste",
"pointsToWinText": "Point for at vinde",
"seriesLengthText": "Serie længde",
"showTutorialText": "Vis tutorial", "showTutorialText": "Vis tutorial",
"shuffleGameOrderText": "Bland rækkefølgen", "shuffleGameOrderText": "Bland spilrækkefølgen",
"titleText": "Tilpas ${TYPE} Spillister" "titleText": "Tilpas ${TYPE} Spillister"
}, },
"gameSettingsWindow": { "gameSettingsWindow": {
@ -709,18 +753,26 @@
"bluetoothJoinText": "Deltag over Bluetooth", "bluetoothJoinText": "Deltag over Bluetooth",
"bluetoothText": "Bluetooth", "bluetoothText": "Bluetooth",
"checkingText": "kontrollerer...", "checkingText": "kontrollerer...",
"copyCodeConfirmText": "Kode kopieret til udklipsholder.",
"copyCodeText": "Kopier kode",
"dedicatedServerInfoText": "For de bedste resultater, lav en dedikeret server. Se bombsquadgame.com/server for at lære hvordan.", "dedicatedServerInfoText": "For de bedste resultater, lav en dedikeret server. Se bombsquadgame.com/server for at lære hvordan.",
"descriptionShortText": "Brug saml-vinduet for at samle et hold.",
"disconnectClientsText": "Dette vil koble ${COUNT} spiller(e) fra\ndin gruppe. Er du sikker?", "disconnectClientsText": "Dette vil koble ${COUNT} spiller(e) fra\ndin gruppe. Er du sikker?",
"earnTicketsForRecommendingAmountText": "Venner vil modtage ${COUNT} billetter hvis de prøver spillet\n(og du vil modtage ${YOU_COUNT} for hver, som gør)", "earnTicketsForRecommendingAmountText": "Venner vil modtage ${COUNT} billetter hvis de prøver spillet\n(og du vil modtage ${YOU_COUNT} for hver, som gør)",
"earnTicketsForRecommendingText": "Del spiller\nfor gratis billetter...", "earnTicketsForRecommendingText": "Del spiller\nfor gratis billetter...",
"emailItText": "Email det", "emailItText": "Email det",
"favoritesSaveText": "Gem som favorit",
"favoritesText": "Favoritter",
"freeCloudServerAvailableMinutesText": "Næste ledige cloud server er klar om ${MINUTES} minuter.",
"freeCloudServerAvailableNowText": "Cloud server ledig!",
"freeCloudServerNotAvailableText": "Ingen cloud servere er ledige.",
"friendHasSentPromoCodeText": "${COUNT} ${APP_NAME} billetter fra ${NAME}", "friendHasSentPromoCodeText": "${COUNT} ${APP_NAME} billetter fra ${NAME}",
"friendPromoCodeAwardText": "Du vil modtage ${COUNT} billetter hver gang det bliver brugt.", "friendPromoCodeAwardText": "Du vil modtage ${COUNT} billetter hver gang det bliver brugt.",
"friendPromoCodeExpireText": "Denne kode vil udløbe om ${EXPIRE_HOURS} timer og vil kun virke for nye spillere.", "friendPromoCodeExpireText": "Denne kode vil udløbe om ${EXPIRE_HOURS} timer og vil kun virke for nye spillere.",
"friendPromoCodeInstructionsText": "For at bruge det, åben ${APP_NAME} og gå ind i \"Indstillinger->Avanceret->Indtast Kode\".\nSe bombsquadgame.com for downloadlinks til alle understøttede platforme.", "friendPromoCodeInstructionsText": "For at bruge det, åben ${APP_NAME} og gå ind i \"Indstillinger->Avanceret->Send Info\".\nSe bombsquadgame.com for downloadlinks til alle understøttede platforme.",
"friendPromoCodeRedeemLongText": "Det kan blive indløst for ${COUNT} gratis billetter for op til ${MAX_USES} personer.", "friendPromoCodeRedeemLongText": "Det kan blive indløst for ${COUNT} gratis billetter for op til ${MAX_USES} personer.",
"friendPromoCodeRedeemShortText": "Det kan blive indløst for ${COUNT} billetter i spillet.", "friendPromoCodeRedeemShortText": "Det kan blive indløst for ${COUNT} billetter i spillet.",
"friendPromoCodeWhereToEnterText": "(i \"Indstillinger->Avanceret->Indtast Kode\")", "friendPromoCodeWhereToEnterText": "(i \"Indstillinger->Avanceret->Send Info\")",
"getFriendInviteCodeText": "Få invitationskode til venner", "getFriendInviteCodeText": "Få invitationskode til venner",
"googlePlayDescriptionText": "Inviter Google Play spillere til din gruppe:", "googlePlayDescriptionText": "Inviter Google Play spillere til din gruppe:",
"googlePlayInviteText": "Inviter", "googlePlayInviteText": "Inviter",
@ -728,19 +780,21 @@
"googlePlaySeeInvitesText": "Se inviterede", "googlePlaySeeInvitesText": "Se inviterede",
"googlePlayText": "Google Play", "googlePlayText": "Google Play",
"googlePlayVersionOnlyText": "(Android / Google Play version)", "googlePlayVersionOnlyText": "(Android / Google Play version)",
"hostPublicPartyDescriptionText": "Lav en offentlig gruppe:", "hostPublicPartyDescriptionText": "Lav en offentlig gruppe",
"hostingUnavailableText": "Hosting ikke muligt",
"inDevelopmentWarningText": "Note:\n\nNetværksspil er en ny og stadig udviklende funktion.\nFor nu, anbefales det stærkt, at alle\nspillere er på det samme WiFi-netværk.", "inDevelopmentWarningText": "Note:\n\nNetværksspil er en ny og stadig udviklende funktion.\nFor nu, anbefales det stærkt, at alle\nspillere er på det samme WiFi-netværk.",
"internetText": "Internet", "internetText": "Internet",
"inviteAFriendText": "Venner har ikke spillet? Inviter de til at\nprøve det og så modtager de ${COUNT} gratis billetter.", "inviteAFriendText": "Venner har ikke spillet? Inviter de til at\nprøve det og så modtager de ${COUNT} gratis billetter.",
"inviteFriendsText": "Inviter venner", "inviteFriendsText": "Inviter venner",
"joinPublicPartyDescriptionText": "Deltag i en offentlig gruppe:", "joinPublicPartyDescriptionText": "Deltag i en offentlig gruppe",
"localNetworkDescriptionText": "Deltag i en gruppe på dit netværk:", "localNetworkDescriptionText": "Deltag i en gruppe på dit netværk (LAN, Bluetooth, osv.)",
"localNetworkText": "Lokalt netværk", "localNetworkText": "Lokalt netværk",
"makePartyPrivateText": "Gør min gruppe privat", "makePartyPrivateText": "Gør min gruppe privat",
"makePartyPublicText": "Gør min gruppe offentlig", "makePartyPublicText": "Gør min gruppe offentlig",
"manualAddressText": "Adresse", "manualAddressText": "Adresse",
"manualConnectText": "Opret forbindelse", "manualConnectText": "Opret forbindelse",
"manualDescriptionText": "Deltag i en gruppe på en adresse:", "manualDescriptionText": "Deltag i en gruppe på en adresse:",
"manualJoinSectionText": "Deltag med en adresse",
"manualJoinableFromInternetText": "Kan du deltage via internettet?:", "manualJoinableFromInternetText": "Kan du deltage via internettet?:",
"manualJoinableNoWithAsteriskText": "NEJ*", "manualJoinableNoWithAsteriskText": "NEJ*",
"manualJoinableYesText": "JA", "manualJoinableYesText": "JA",
@ -748,14 +802,18 @@
"manualText": "Manual", "manualText": "Manual",
"manualYourAddressFromInternetText": "Din adresse fra internettet:", "manualYourAddressFromInternetText": "Din adresse fra internettet:",
"manualYourLocalAddressText": "Din lokale adresse:", "manualYourLocalAddressText": "Din lokale adresse:",
"nearbyText": "Nær",
"noConnectionText": "<ingen forbindelse>", "noConnectionText": "<ingen forbindelse>",
"noPartiesAddedText": "Ingen grupper tilføjet",
"otherVersionsText": "(andre versioner)", "otherVersionsText": "(andre versioner)",
"partyCodeText": "Gruppekode",
"partyInviteAcceptText": "Accepter", "partyInviteAcceptText": "Accepter",
"partyInviteDeclineText": "Afvis", "partyInviteDeclineText": "Afvis",
"partyInviteGooglePlayExtraText": "(Se 'Google Play' fanen i 'Saml' vinduet)", "partyInviteGooglePlayExtraText": "(Se 'Google Play' fanen i 'Saml' vinduet)",
"partyInviteIgnoreText": "Ignorer", "partyInviteIgnoreText": "Ignorer",
"partyInviteText": "${NAME} har inviteret\ndig til at deltage i deres gruppe!", "partyInviteText": "${NAME} har inviteret\ndig til at deltage i deres gruppe!",
"partyNameText": "Gruppe navn", "partyNameText": "Gruppe navn",
"partyServerRunningText": "Din gruppeserver kører.",
"partySizeText": "gruppe størrelse", "partySizeText": "gruppe størrelse",
"partyStatusCheckingText": "kontrollerer status...", "partyStatusCheckingText": "kontrollerer status...",
"partyStatusJoinableText": "din gruppe kan nu tilsluttes fra internettet", "partyStatusJoinableText": "din gruppe kan nu tilsluttes fra internettet",
@ -764,10 +822,20 @@
"partyStatusNotPublicText": "din gruppe er ikke offentlig", "partyStatusNotPublicText": "din gruppe er ikke offentlig",
"pingText": "ping", "pingText": "ping",
"portText": "Port", "portText": "Port",
"privatePartyCloudDescriptionText": "Private grupper kører på dedikerede cloud servers; ingen router konfiguration er nødvendig.",
"privatePartyHostText": "Host Privat Gruppe",
"privatePartyJoinText": "Deltag i en Privat Gruppe",
"privateText": "Privat",
"publicHostRouterConfigText": "Dette kræver måske konfiguration af port-forwarding på din router. En lettere løsning er at hoste en Privat Gruppe.",
"publicText": "Offentlig",
"requestingAPromoCodeText": "Anmoder om kode...", "requestingAPromoCodeText": "Anmoder om kode...",
"sendDirectInvitesText": "Send direkte invitationer", "sendDirectInvitesText": "Send direkte invitationer",
"shareThisCodeWithFriendsText": "Del denne kode med dine venner:", "shareThisCodeWithFriendsText": "Del denne kode med dine venner:",
"showMyAddressText": "Vis min adresse", "showMyAddressText": "Vis min adresse",
"startHostingPaidText": "Host nu for ${COST}",
"startHostingText": "Host",
"startStopHostingMinutesText": "Du kan starte og stoppe med at hoste gratis de næste ${MINUTES} minutter.",
"stopHostingText": "Stop med at hoste",
"titleText": "Saml", "titleText": "Saml",
"wifiDirectDescriptionBottomText": "Hvis alle enheder har et 'WiFi Direkte' panel, skulle de være i stand til at bruge det til at finde\nog forbinde til hinanden. Når alle enheder er tilsluttet, kan du danne grupper\nher ved hjælp af fanen 'Lokalt netværk', præcis på samme måde som med et almindeligt WiFi-netværk.\n\nFor de bedste resultater skal WiFi Direct værten også være ${APP_NAME} gruppeværten.", "wifiDirectDescriptionBottomText": "Hvis alle enheder har et 'WiFi Direkte' panel, skulle de være i stand til at bruge det til at finde\nog forbinde til hinanden. Når alle enheder er tilsluttet, kan du danne grupper\nher ved hjælp af fanen 'Lokalt netværk', præcis på samme måde som med et almindeligt WiFi-netværk.\n\nFor de bedste resultater skal WiFi Direct værten også være ${APP_NAME} gruppeværten.",
"wifiDirectDescriptionTopText": "WiFi Direkte kan bruges til at forbinde Android-enheder direkte uden\nbrug for et WiFi-netværk. Dette fungerer bedst på Android 4.2 eller nyere.\n\nFor at bruge det skal du åbne WiFi-indstillingerne og kigge efter 'WiFi Direkte' i menuen.", "wifiDirectDescriptionTopText": "WiFi Direkte kan bruges til at forbinde Android-enheder direkte uden\nbrug for et WiFi-netværk. Dette fungerer bedst på Android 4.2 eller nyere.\n\nFor at bruge det skal du åbne WiFi-indstillingerne og kigge efter 'WiFi Direkte' i menuen.",
@ -797,7 +865,7 @@
"ticketPack4Text": "Jumbo billetpakke", "ticketPack4Text": "Jumbo billetpakke",
"ticketPack5Text": "Mammut billetpakke", "ticketPack5Text": "Mammut billetpakke",
"ticketPack6Text": "Ultimativ billetpakke", "ticketPack6Text": "Ultimativ billetpakke",
"ticketsFromASponsorText": "Få ${COUNT} billetter\nfra en sponsor", "ticketsFromASponsorText": "Se en reklame\nfor at få ${COUNT} billetter",
"ticketsText": "${COUNT} billetter", "ticketsText": "${COUNT} billetter",
"titleText": "Få billetter", "titleText": "Få billetter",
"unavailableLinkAccountText": "Beklager, køb er ikke tilgængeligt på denne platform.\nSom en løsning kan du forbinde denne konto til en anden konto på\nen anden platform og foretage købene der.", "unavailableLinkAccountText": "Beklager, køb er ikke tilgængeligt på denne platform.\nSom en løsning kan du forbinde denne konto til en anden konto på\nen anden platform og foretage købene der.",

View file

@ -1706,6 +1706,7 @@
"Arabic": "Arabisch", "Arabic": "Arabisch",
"Belarussian": "Belarusian", "Belarussian": "Belarusian",
"Chinese": "Vereenvoudigd Chinees ", "Chinese": "Vereenvoudigd Chinees ",
"ChineseSimplified": "Vereenvoudigd Chinees",
"ChineseTraditional": "Traditioneel Chinees", "ChineseTraditional": "Traditioneel Chinees",
"Croatian": "Kroatisch", "Croatian": "Kroatisch",
"Czech": "Tsjechisch", "Czech": "Tsjechisch",
@ -1730,11 +1731,15 @@
"PirateSpeak": "Piraat Praat", "PirateSpeak": "Piraat Praat",
"Polish": "Pools", "Polish": "Pools",
"Portuguese": "Portugees", "Portuguese": "Portugees",
"PortugueseBrazil": "Portugees - Brazilië",
"PortuguesePortugal": "Portugees - Portugal",
"Romanian": "Roemeens", "Romanian": "Roemeens",
"Russian": "Russisch", "Russian": "Russisch",
"Serbian": "Servisch", "Serbian": "Servisch",
"Slovak": "Sloveens", "Slovak": "Sloveens",
"Spanish": "Spaans", "Spanish": "Spaans",
"SpanishLatinAmerica": "Spaans - Latijns Amerika",
"SpanishSpain": "Spaans - Spanje",
"Swedish": "Zweeds", "Swedish": "Zweeds",
"Tamil": "Tamil", "Tamil": "Tamil",
"Thai": "Thais", "Thai": "Thais",

View file

@ -1632,6 +1632,7 @@
"Indonesian": null, "Indonesian": null,
"Italian": null, "Italian": null,
"Japanese": null, "Japanese": null,
"Kazakh": null,
"Korean": null, "Korean": null,
"Malay": null, "Malay": null,
"Persian": null, "Persian": null,
@ -1763,6 +1764,7 @@
"You got an achievement reward!": null, "You got an achievement reward!": null,
"You have been promoted to a new league; congratulations!": null, "You have been promoted to a new league; congratulations!": null,
"You lost a chest! (All your chest slots were full)": null, "You lost a chest! (All your chest slots were full)": null,
"You must sign in to do this.": null,
"You must update the app to view this.": null, "You must update the app to view this.": null,
"You must update to a newer version of the app to do this.": null, "You must update to a newer version of the app to do this.": null,
"You must update to the newest version of the game to do this.": null, "You must update to the newest version of the game to do this.": null,

View file

@ -902,7 +902,7 @@
"powerupPunchDescriptionText": "Ang iyong mga suntok ay mas mahirap,\nmas mabilis, mas mahusay, at mas malakas.", "powerupPunchDescriptionText": "Ang iyong mga suntok ay mas mahirap,\nmas mabilis, mas mahusay, at mas malakas.",
"powerupPunchNameText": "Guwantes", "powerupPunchNameText": "Guwantes",
"powerupShieldDescriptionText": "Pumigil na pagsakit\nPara mas guminhawa.", "powerupShieldDescriptionText": "Pumigil na pagsakit\nPara mas guminhawa.",
"powerupShieldNameText": "Enrhiyang-Kalasag", "powerupShieldNameText": "Enerhiyang-Kalasag",
"powerupStickyBombsDescriptionText": "Dumikit sa anumang matamaan nila.\nItoy naging pagtawanan.", "powerupStickyBombsDescriptionText": "Dumikit sa anumang matamaan nila.\nItoy naging pagtawanan.",
"powerupStickyBombsNameText": "Bombang-Malagkit", "powerupStickyBombsNameText": "Bombang-Malagkit",
"powerupsSubtitleText": "Siyempre, 'di kumpleto ang laro kapag walang mga powerups:", "powerupsSubtitleText": "Siyempre, 'di kumpleto ang laro kapag walang mga powerups:",
@ -928,7 +928,7 @@
"internal": { "internal": {
"arrowsToExitListText": "pindutin ang ${LEFT} o ${RIGHT} upang mawala 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 mapatalsikin ang host.",
"chatBlockedText": "Na-block si ${NAME} sa loob ng ${TIME} segundo.", "chatBlockedText": "Na-block si ${NAME} sa loob ng ${TIME} segundo.",
"connectedToGameText": "Sumali sa '${NAME}'", "connectedToGameText": "Sumali sa '${NAME}'",
"connectedToPartyText": "Sumali sa party ni ${NAME}!", "connectedToPartyText": "Sumali sa party ni ${NAME}!",
@ -981,7 +981,7 @@
"unableToCompleteTryAgainText": "Hindi maaaring maitapos ito sa ngayon.\nMaaaring mo ulitin.", "unableToCompleteTryAgainText": "Hindi maaaring maitapos ito sa ngayon.\nMaaaring mo ulitin.",
"unableToResolveHostText": "Error: hindi malutas ang host.", "unableToResolveHostText": "Error: hindi malutas ang host.",
"unavailableNoConnectionText": "Itoy kasalukuyang hindi magagamit (walang koneksyon sa internet?)", "unavailableNoConnectionText": "Itoy kasalukuyang hindi magagamit (walang koneksyon sa internet?)",
"vrOrientationResetCardboardText": "Gamitin ito upang i-reset ang oryentasyon ng VR.\nUpang maglaro ng laro kakailanganin mo ng isang panlabas na controller.", "vrOrientationResetCardboardText": "Gamitin ito upang i-reset ang oryentasyon ng VR.\nUpang maglaro ng laro kakailanganin mo ng isang external na controller.",
"vrOrientationResetText": "Pag-reset ng oryentasyon ng VR.", "vrOrientationResetText": "Pag-reset ng oryentasyon ng VR.",
"willTimeOutText": "(magta-time out kung idle)" "willTimeOutText": "(magta-time out kung idle)"
}, },
@ -993,16 +993,16 @@
"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": "Napalayas si ${NAME} dito.", "kickOccurredText": "Natalsik na si ${NAME} dito.",
"kickQuestionText": "Palayasin si ${NAME}?", "kickQuestionText": "Talsikin si ${NAME}?",
"kickText": "Palayasin", "kickText": "Patalsikin",
"kickVoteCantKickAdminsText": "Hindi pwedeng palayasin ang Mga Ninuno dito.", "kickVoteCantKickAdminsText": "Hindi pwedeng palayasin ang Mga Ninuno dito.",
"kickVoteCantKickSelfText": "Hindi mo mailayas ang iyong sarili.", "kickVoteCantKickSelfText": "Hindi mo mailayas ang iyong sarili.",
"kickVoteFailedNotEnoughVotersText": "Konti ang mga naglalaro upang magbutuhan.", "kickVoteFailedNotEnoughVotersText": "Konti ang mga naglalaro upang magbutuhan.",
"kickVoteFailedText": "Hindi maipalayas mula sa pagbutuhan.", "kickVoteFailedText": "Hindi maipalayas mula sa pagbutuhan.",
"kickVoteStartedText": "Sinimulan ang butuhan upang palayasin dito si ${NAME}.", "kickVoteStartedText": "Sinimulan ang butuhan upang patalsikin dito si ${NAME}.",
"kickVoteText": "Bumoto para Ipalayas", "kickVoteText": "Bumoto para Patalsikin",
"kickVotingDisabledText": "Naka-disable ang kick voting.", "kickVotingDisabledText": "Nakasara ang boto sa pagtalsikin.",
"kickWithChatText": "Pindutin ang ${YES} sa iyong puwang salitahan kung oo at ${NO} kung hindi.", "kickWithChatText": "Pindutin ang ${YES} sa iyong puwang salitahan kung oo at ${NO} kung hindi.",
"killsTallyText": "${COUNT} pinatay", "killsTallyText": "${COUNT} pinatay",
"killsText": "Pinatay", "killsText": "Pinatay",
@ -1177,7 +1177,7 @@
"titleText": "Karakter ng Manlalaro" "titleText": "Karakter ng Manlalaro"
}, },
"playerText": "Manlalaro", "playerText": "Manlalaro",
"playlistNoValidGamesErrorText": "Ang playlist na ito ay hindi naglalaman ng mga wastong 'di maibukas na laro.", "playlistNoValidGamesErrorText": "Ang playlist na ito ay hindi naglalaman ng mga nabubukas na laro.",
"playlistNotFoundText": "hindi nahanap ang playlist", "playlistNotFoundText": "hindi nahanap ang playlist",
"playlistText": "Playlist", "playlistText": "Playlist",
"playlistsText": "Mga Playlist", "playlistsText": "Mga Playlist",
@ -1312,7 +1312,7 @@
}, },
"settingsWindowAdvanced": { "settingsWindowAdvanced": {
"alwaysUseInternalKeyboardDescriptionText": "(isang simpleng keyboard na nasa loob lamang ng larong ito para sa pagasasaayos ng teksto)", "alwaysUseInternalKeyboardDescriptionText": "(isang simpleng keyboard na nasa loob lamang ng larong ito para sa pagasasaayos ng teksto)",
"alwaysUseInternalKeyboardText": "Laging gumamit ang keyboard na panloob", "alwaysUseInternalKeyboardText": "Laging gumamit ang keyboard na internal",
"benchmarksText": "Mga Benchmark at Pagsusuri sa Pagdiin", "benchmarksText": "Mga Benchmark at Pagsusuri sa Pagdiin",
"devToolsText": "Kagamitan sa Paggawa", "devToolsText": "Kagamitan sa Paggawa",
"disableCameraGyroscopeMotionText": "Itigil ang mosyong dyayroskop ng larawan", "disableCameraGyroscopeMotionText": "Itigil ang mosyong dyayroskop ng larawan",
@ -1325,7 +1325,7 @@
"helpTranslateText": "Ang mga pagsasalin na bukod sa Ingles ng ${APP_NAME} ay isang pagsisikap na \nsuporta ng komunidad dito sa larong ito. Kung gusto mong mag-ambag o magtama \nng isa o higit pang pagsasalin, sundan ang link sa ibaba. Salamat po muli!", "helpTranslateText": "Ang mga pagsasalin na bukod sa Ingles ng ${APP_NAME} ay isang pagsisikap na \nsuporta ng komunidad dito sa larong ito. Kung gusto mong mag-ambag o magtama \nng isa o higit pang pagsasalin, sundan ang link sa ibaba. Salamat po muli!",
"insecureConnectionsDescriptionText": "Hindi ito irerekomenda, ngunit pwede maglaro nang online \nmula sa ibang bansa o network na limitado.", "insecureConnectionsDescriptionText": "Hindi ito irerekomenda, ngunit pwede maglaro nang online \nmula sa ibang bansa o network na limitado.",
"insecureConnectionsText": "Gumamit ng koneksyong walang seguridad", "insecureConnectionsText": "Gumamit ng koneksyong walang seguridad",
"kickIdlePlayersText": "Ipalayas ang mga manlalarong hindi gumagalaw", "kickIdlePlayersText": "Talsikin ang mga manlalarong hindi gumagalaw",
"kidFriendlyModeText": "Pamamaraang Pambata (walang tinding karahasan, atbp)", "kidFriendlyModeText": "Pamamaraang Pambata (walang tinding karahasan, atbp)",
"languageText": "Wika", "languageText": "Wika",
"moddingGuideText": "Gabay sa Pagbabago (Mod)", "moddingGuideText": "Gabay sa Pagbabago (Mod)",
@ -1402,7 +1402,7 @@
"howToUseMapsText": "(gamitin ang mga mapa na ito sa sarili mong Mga Team/Rambulan na mga playlist)", "howToUseMapsText": "(gamitin ang mga mapa na ito sa sarili mong Mga Team/Rambulan na mga playlist)",
"iconsText": "Mga Tatak", "iconsText": "Mga Tatak",
"loadErrorText": "Hindi ma-load ang page.\nSuriin ang iyong koneksyon sa internet.", "loadErrorText": "Hindi ma-load ang page.\nSuriin ang iyong koneksyon sa internet.",
"loadingText": "Saglit lang…", "loadingText": "nagloload…",
"mapsText": "Mga Mapa", "mapsText": "Mga Mapa",
"miniGamesText": "Mga Lalaruhin", "miniGamesText": "Mga Lalaruhin",
"oneTimeOnlyText": "(isang beses lang)", "oneTimeOnlyText": "(isang beses lang)",
@ -1642,6 +1642,7 @@
"Indonesian": "Wikang Indonesiyo", "Indonesian": "Wikang Indonesiyo",
"Italian": "Wikang Italiyano", "Italian": "Wikang Italiyano",
"Japanese": "Wikang Hapon", "Japanese": "Wikang Hapon",
"Kazakh": "Wikang Kazakh",
"Korean": "Wikang Koreano", "Korean": "Wikang Koreano",
"Malay": "Wikang Malay", "Malay": "Wikang Malay",
"Persian": "Wikang Persyano", "Persian": "Wikang Persyano",
@ -1754,7 +1755,7 @@
"Temporarily unavailable; please try again later.": "Pansamantalang hindi magagamit; Subukang muli mamaya.", "Temporarily unavailable; please try again later.": "Pansamantalang hindi magagamit; Subukang muli mamaya.",
"The tournament ended before you finished.": "Natapos ang tournament bago ka natapos.", "The tournament ended before you finished.": "Natapos ang tournament bago ka natapos.",
"This account cannot be unlinked for ${NUM} days.": "Ang account na ito ay hindi maaaring i-unlink sa loob ng ${NUM} (na) araw.", "This account cannot be unlinked for ${NUM} days.": "Ang account na ito ay hindi maaaring i-unlink sa loob ng ${NUM} (na) araw.",
"This code cannot be used on the account that created it.": "Hindi magagamit ang code na ito sa account na lumikha nito.", "This code cannot be used on the account that created it.": "Hindi magagamit ang code na ito sa account na gumawa nito.",
"This is currently unavailable; please try again later.": "Ito ay kasalukuyang hindi magagamit; Subukang muli mamaya.", "This is currently unavailable; please try again later.": "Ito ay kasalukuyang hindi magagamit; Subukang muli mamaya.",
"This requires version ${VERSION} or newer.": "Nangangailangan ito ng bersyon na ${VERSION} o mas bago.", "This requires version ${VERSION} or newer.": "Nangangailangan ito ng bersyon na ${VERSION} o mas bago.",
"Tournaments disabled due to rooted device.": "Na-disable ang mga tournament dahil sa na-root na device.", "Tournaments disabled due to rooted device.": "Na-disable ang mga tournament dahil sa na-root na device.",
@ -1771,10 +1772,11 @@
"You got ${COUNT} tickets!": "Nakakuha ka ng ${COUNT} tickets!", "You got ${COUNT} tickets!": "Nakakuha ka ng ${COUNT} tickets!",
"You got ${COUNT} tokens!": "May nakuha ka na ${COUNT} tokens!", "You got ${COUNT} tokens!": "May nakuha ka na ${COUNT} tokens!",
"You got a ${ITEM}!": "Nakakuha ka ng ${ITEM}!", "You got a ${ITEM}!": "Nakakuha ka ng ${ITEM}!",
"You got a chest!": "Mayroon na ka chest!", "You got a chest!": "Mayroon ka na ng baul!",
"You got an achievement reward!": "May bago kang gantimpala sa tagumpay!", "You got an achievement reward!": "May bago kang gantimpala sa tagumpay!",
"You have been promoted to a new league; congratulations!": "Na-promote ka sa isang bagong liga; congrats!", "You have been promoted to a new league; congratulations!": "Na-promote ka sa isang bagong liga; congrats!",
"You lost a chest! (All your chest slots were full)": "May nawalan ka na chest! (Lahat ng chest slot mo ay puno)", "You lost a chest! (All your chest slots were full)": "May nawalan ka na chest! (Lahat ng chest slot mo ay puno)",
"You must sign in to do this.": "Kailangan pumasok sa account upang magawa ito.",
"You must update the app to view this.": "Kailangan mong baguhin muna ang bersyon ng app na ito upang maitignan ito.", "You must update the app to view this.": "Kailangan mong baguhin muna ang bersyon ng app na ito upang maitignan ito.",
"You must update to a newer version of the app to do this.": "Kailangang magbago sa mas mabagong bersyon ng app para magawa ito.", "You must update to a newer version of the app to do this.": "Kailangang magbago sa mas mabagong bersyon ng app para magawa ito.",
"You must update to the newest version of the game to do this.": "Kailangang magbago sa pinakabagong bersyon ng laro upang magawa ito.", "You must update to the newest version of the game to do this.": "Kailangang magbago sa pinakabagong bersyon ng laro upang magawa ito.",
@ -2003,11 +2005,11 @@
}, },
"winsPlayerText": "Nanalo si ${NAME}!", "winsPlayerText": "Nanalo si ${NAME}!",
"winsTeamText": "Nanalo ang ${NAME}!", "winsTeamText": "Nanalo ang ${NAME}!",
"winsText": "${NAME} Nanalo!", "winsText": "Nanalo si ${NAME}!",
"workspaceSyncErrorText": "Error sa pag-sync ng ${WORKSPACE}. Tingnan ang log para sa mga detalye.", "workspaceSyncErrorText": "Error sa pag-sync ng ${WORKSPACE}. Tingnan ang log para sa mga detalye.",
"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": "Hindi makuha ang mga pandaigdigang iskor.", "worldScoresUnavailableText": "Hindi makuha ang mga pandaigdigang iskor.",
"worldsBestScoresText": "Pinakamahusay na Iskor sa Mundo", "worldsBestScoresText": "Pinakamataas na Iskor sa Mundo",
"worldsBestTimesText": "Oras ng Pinakamabilis sa Mundo", "worldsBestTimesText": "Oras ng Pinakamabilis sa Mundo",
"xbox360ControllersWindow": { "xbox360ControllersWindow": {
"getDriverText": "Kunin ang Driver", "getDriverText": "Kunin ang Driver",

View file

@ -1715,7 +1715,8 @@
"Arabic": "Arabe", "Arabic": "Arabe",
"Belarussian": "Biélorusse", "Belarussian": "Biélorusse",
"Chinese": "Chinois simplifié", "Chinese": "Chinois simplifié",
"ChineseTraditional": "Chinois Traditionnel", "ChineseSimplified": "Chinois - Simplifié",
"ChineseTraditional": "Chinois - Traditionnel",
"Croatian": "Croate", "Croatian": "Croate",
"Czech": "Tchèque", "Czech": "Tchèque",
"Danish": "Danois", "Danish": "Danois",
@ -1733,17 +1734,22 @@
"Indonesian": "Indonésien", "Indonesian": "Indonésien",
"Italian": "Italien", "Italian": "Italien",
"Japanese": "Japonais", "Japanese": "Japonais",
"Kazakh": "Kazakh",
"Korean": "Coréen", "Korean": "Coréen",
"Malay": "Malais", "Malay": "Malais",
"Persian": "Persan", "Persian": "Persan",
"PirateSpeak": "Parole de pirate", "PirateSpeak": "Parole de pirate",
"Polish": "Polonais", "Polish": "Polonais",
"Portuguese": "Portugais", "Portuguese": "Portugais",
"PortugueseBrazil": "Portugais - Brésil",
"PortuguesePortugal": "Portugais - Portugal",
"Romanian": "Roumain", "Romanian": "Roumain",
"Russian": "Russe", "Russian": "Russe",
"Serbian": "Serbe", "Serbian": "Serbe",
"Slovak": "Slovaque", "Slovak": "Slovaque",
"Spanish": "Espagnol", "Spanish": "Espagnol",
"SpanishLatinAmerica": "Espagnol - Amérique Latine",
"SpanishSpain": "Espagnol - Espagne",
"Swedish": "Suédois", "Swedish": "Suédois",
"Tamil": "Tamil", "Tamil": "Tamil",
"Thai": "Thaïlandais", "Thai": "Thaïlandais",

View file

@ -1738,7 +1738,8 @@
"Arabic": "Arabisch", "Arabic": "Arabisch",
"Belarussian": "Weißrussland", "Belarussian": "Weißrussland",
"Chinese": "Chinesisch vereinfacht", "Chinese": "Chinesisch vereinfacht",
"ChineseTraditional": "Chinesisch Traditionell", "ChineseSimplified": "Chinesisch - Vereinfacht",
"ChineseTraditional": "Chinesisch - Traditionell",
"Croatian": "Kroatisch", "Croatian": "Kroatisch",
"Czech": "Tschechisch", "Czech": "Tschechisch",
"Danish": "Dänisch", "Danish": "Dänisch",
@ -1756,17 +1757,22 @@
"Indonesian": "Indonesisch", "Indonesian": "Indonesisch",
"Italian": "Italienisch", "Italian": "Italienisch",
"Japanese": "Japanisch", "Japanese": "Japanisch",
"Kazakh": "Kasachisch",
"Korean": "Koreanisch", "Korean": "Koreanisch",
"Malay": "Malaiisch", "Malay": "Malaiisch",
"Persian": "Persisch", "Persian": "Persisch",
"PirateSpeak": "Piratensprache", "PirateSpeak": "Piratensprache",
"Polish": "Polnisch", "Polish": "Polnisch",
"Portuguese": "Portugiesisch", "Portuguese": "Portugiesisch",
"PortugueseBrazil": "Portugiesisch - Brasilien",
"PortuguesePortugal": "Portugiesisch - Portugal",
"Romanian": "Rumänisch", "Romanian": "Rumänisch",
"Russian": "Russisch", "Russian": "Russisch",
"Serbian": "Serbisch", "Serbian": "Serbisch",
"Slovak": "Slovakisch", "Slovak": "Slovakisch",
"Spanish": "Spanisch", "Spanish": "Spanisch",
"SpanishLatinAmerica": "Spanisch - Lateinamerika",
"SpanishSpain": "Spanisch - Spanien",
"Swedish": "Schwedisch", "Swedish": "Schwedisch",
"Tamil": "Tamil", "Tamil": "Tamil",
"Thai": "Thailändisch", "Thai": "Thailändisch",

View file

@ -1787,6 +1787,7 @@
"Indonesian": "Inofiqdson", "Indonesian": "Inofiqdson",
"Italian": "Itzllfjssnn", "Italian": "Itzllfjssnn",
"Japanese": "Capnokas", "Japanese": "Capnokas",
"Kazakh": "Kfwefz",
"Korean": "Kornesnzn", "Korean": "Kornesnzn",
"Malay": "FJwoerjjdf", "Malay": "FJwoerjjdf",
"Persian": "Psdfsdf", "Persian": "Psdfsdf",
@ -1924,6 +1925,7 @@
"You got an achievement reward!": "You cowe owf woef weg;oai cower jower!", "You got an achievement reward!": "You cowe owf woef weg;oai cower jower!",
"You have been promoted to a new league; congratulations!": "Yf co efoj woef woecj owejfoiwef lfj ; congaroiwjf woes!", "You have been promoted to a new league; congratulations!": "Yf co efoj woef woecj owejfoiwef lfj ; congaroiwjf woes!",
"You lost a chest! (All your chest slots were full)": "Ycowe wo sowfweo ! (ALlco weofi wf o; cwoer orul)", "You lost a chest! (All your chest slots were full)": "Ycowe wo sowfweo ! (ALlco weofi wf o; cwoer orul)",
"You must sign in to do this.": "You mfw sdf gose f wr weorijdfs.",
"You must update the app to view this.": "Yowu cow woef jwowo ejwe;oij wofjwoed.", "You must update the app to view this.": "Yowu cow woef jwowo ejwe;oij wofjwoed.",
"You must update to a newer version of the app to do this.": "Yz mocu upc oig owc owt oc o ca; ;apc oi oj ;oj.", "You must update to a newer version of the app to do this.": "Yz mocu upc oig owc owt oc o ca; ;apc oi oj ;oj.",
"You must update to the newest version of the game to do this.": "Yocwe fweoowo weotjosij;ow. woeower weroso taotoautats.", "You must update to the newest version of the game to do this.": "Yocwe fweoowo weotjosij;ow. woeower weroso taotoautats.",

View file

@ -1628,7 +1628,8 @@
"Arabic": "अरबी", "Arabic": "अरबी",
"Belarussian": "बेलारूसी", "Belarussian": "बेलारूसी",
"Chinese": "सरलीकृत चीनी", "Chinese": "सरलीकृत चीनी",
"ChineseTraditional": "चीनी पारंपरिक", "ChineseSimplified": "चीनी - सरलीकृत",
"ChineseTraditional": "चीनी - परंपरागत",
"Croatian": "क्रोएशियाई", "Croatian": "क्रोएशियाई",
"Czech": "चेक", "Czech": "चेक",
"Danish": "डेनिश", "Danish": "डेनिश",
@ -1652,11 +1653,15 @@
"PirateSpeak": "डाकू भाषा", "PirateSpeak": "डाकू भाषा",
"Polish": "पोलिश", "Polish": "पोलिश",
"Portuguese": "पुर्तगाली", "Portuguese": "पुर्तगाली",
"PortugueseBrazil": "पुर्तगाली - ब्राज़ील",
"PortuguesePortugal": "पुर्तगाली - पुर्तगाल",
"Romanian": "रोमानियाई", "Romanian": "रोमानियाई",
"Russian": "रूसी", "Russian": "रूसी",
"Serbian": "सर्बियाई", "Serbian": "सर्बियाई",
"Slovak": "स्लोवाक", "Slovak": "स्लोवाक",
"Spanish": "स्पेनिश", "Spanish": "स्पेनिश",
"SpanishLatinAmerica": "स्पेनिश - लैटिन अमेरिका",
"SpanishSpain": "स्पेनिश - स्पेन",
"Swedish": "स्वीडिश", "Swedish": "स्वीडिश",
"Tamil": "तामिल", "Tamil": "तामिल",
"Thai": "थाई", "Thai": "थाई",

View file

@ -1628,7 +1628,8 @@
"Arabic": "Arab", "Arabic": "Arab",
"Belarussian": "Belarusia", "Belarussian": "Belarusia",
"Chinese": "Mandarin (disederhanakan) ", "Chinese": "Mandarin (disederhanakan) ",
"ChineseTraditional": "Mandarin Tradisional", "ChineseSimplified": "Mandarin - Disederhanakan",
"ChineseTraditional": "Mandarin - Tradisional",
"Croatian": "Kroasia", "Croatian": "Kroasia",
"Czech": "Ceko", "Czech": "Ceko",
"Danish": "Denmark", "Danish": "Denmark",
@ -1646,17 +1647,22 @@
"Indonesian": "Bahasa Indonesia", "Indonesian": "Bahasa Indonesia",
"Italian": "Italia", "Italian": "Italia",
"Japanese": "Jepang", "Japanese": "Jepang",
"Kazakh": "Kazakh",
"Korean": "Korea", "Korean": "Korea",
"Malay": "Bahasa Malaysia", "Malay": "Bahasa Malaysia",
"Persian": "Persia", "Persian": "Persia",
"PirateSpeak": "Omongan Bajak Laut", "PirateSpeak": "Logat Bajak Laut",
"Polish": "Polandia", "Polish": "Polandia",
"Portuguese": "Portugis", "Portuguese": "Portugis",
"PortugueseBrazil": "Portugis - Brazil",
"PortuguesePortugal": "Portugis - Portugal",
"Romanian": "Romania", "Romanian": "Romania",
"Russian": "Rusia", "Russian": "Rusia",
"Serbian": "Serbia", "Serbian": "Serbia",
"Slovak": "Slovakia", "Slovak": "Slovakia",
"Spanish": "Spanyol", "Spanish": "Spanyol",
"SpanishLatinAmerica": "Spanyol - Amerika Latin",
"SpanishSpain": "Spanyol",
"Swedish": "Swedia", "Swedish": "Swedia",
"Tamil": "Tamil", "Tamil": "Tamil",
"Thai": "Thai", "Thai": "Thai",
@ -1819,8 +1825,8 @@
"None": "Tak Satupun", "None": "Tak Satupun",
"Normal": "Normal", "Normal": "Normal",
"Pro Mode": "Mode Ahli", "Pro Mode": "Mode Ahli",
"Respawn Times": "Muncul Kembali Hingga", "Respawn Times": "Hidup Kembali Hingga",
"Score to Win": "Skor Menang", "Score to Win": "Skor 'tuk Menang",
"Short": "Pendek", "Short": "Pendek",
"Shorter": "Pendek Sekali", "Shorter": "Pendek Sekali",
"Solo Mode": "Mode Tunggal", "Solo Mode": "Mode Tunggal",
@ -1926,7 +1932,7 @@
"randomName4Text": "Udin", "randomName4Text": "Udin",
"randomName5Text": "Agus", "randomName5Text": "Agus",
"skipConfirmText": "Yakin ingin melompati pembelajaran? Tekan apa saja untuk konfirmasi.", "skipConfirmText": "Yakin ingin melompati pembelajaran? Tekan apa saja untuk konfirmasi.",
"skipVoteCountText": "${COUNT} dari ${TOTAL} suara untuk melewati latihan", "skipVoteCountText": "${COUNT} dari ${TOTAL} suara untuk lewati",
"skippingText": "melewati latihan...", "skippingText": "melewati latihan...",
"toSkipPressAnythingText": "(tekan apa saja untuk melompati pembelajaran)" "toSkipPressAnythingText": "(tekan apa saja untuk melompati pembelajaran)"
}, },
@ -1935,7 +1941,7 @@
"unavailableText": "tidak tersedia", "unavailableText": "tidak tersedia",
"unclaimedPrizesText": "Kamu punya hadiah yang belum diterima!", "unclaimedPrizesText": "Kamu punya hadiah yang belum diterima!",
"unconfiguredControllerDetectedText": "Pengontrol belum terkonfigurasi terdeteksi:", "unconfiguredControllerDetectedText": "Pengontrol belum terkonfigurasi terdeteksi:",
"unlockThisInTheStoreText": "Tersedia di toko terdekat.", "unlockThisInTheStoreText": "Ini harus dibuka melalui toko.",
"unlockThisProfilesText": "Untuk membuat lebih dari ${NUM} profil, kamu membutuhkan:", "unlockThisProfilesText": "Untuk membuat lebih dari ${NUM} profil, kamu membutuhkan:",
"unlockThisText": "Untuk buka ini, kamu membutuhkan:", "unlockThisText": "Untuk buka ini, kamu membutuhkan:",
"unsupportedControllerText": "Maaf, pengontrol \"${NAME}\" tidak didukung.", "unsupportedControllerText": "Maaf, pengontrol \"${NAME}\" tidak didukung.",
@ -1953,7 +1959,7 @@
"usingItunesTurnRepeatAndShuffleOnText": "Tolong pastikan lagu diacak dan diulang di iTunes. ", "usingItunesTurnRepeatAndShuffleOnText": "Tolong pastikan lagu diacak dan diulang di iTunes. ",
"v2AccountLinkingInfoText": "Untuk menautkan akun V2, gunakan tombol 'Manajemen Akun'.", "v2AccountLinkingInfoText": "Untuk menautkan akun V2, gunakan tombol 'Manajemen Akun'.",
"v2AccountRequiredText": "Membutuhkan akun V2. Upgrade akunmu dan coba lagi.", "v2AccountRequiredText": "Membutuhkan akun V2. Upgrade akunmu dan coba lagi.",
"validatingTestBuildText": "Memvalidasi Tes Build...", "validatingTestBuildText": "Memvalidasi Build Percobaan...",
"viaText": "melalui", "viaText": "melalui",
"victoryText": "Menang!", "victoryText": "Menang!",
"voteDelayText": "Kamu tidak dapat memulai pemilihan suara dalam ${NUMBER} detik", "voteDelayText": "Kamu tidak dapat memulai pemilihan suara dalam ${NUMBER} detik",
@ -1968,19 +1974,19 @@
"watchAnAdText": "Tonton Iklan", "watchAnAdText": "Tonton Iklan",
"watchWindow": { "watchWindow": {
"deleteConfirmText": "Hapus \"${REPLAY}\"?", "deleteConfirmText": "Hapus \"${REPLAY}\"?",
"deleteReplayButtonText": "Hapus\nReplay", "deleteReplayButtonText": "Hapus\nRekaman",
"myReplaysText": "Replayku", "myReplaysText": "Rekamanku",
"noReplaySelectedErrorText": "Tidak Ada Replay Terpilih", "noReplaySelectedErrorText": "Tidak Ada Rekaman Terpilih",
"playbackSpeedText": "Kecepatan Pemutaran: ${SPEED}", "playbackSpeedText": "Kecepatan Pemutaran: ${SPEED}",
"renameReplayButtonText": "Ganti Nama\nReplay", "renameReplayButtonText": "Ganti Nama\nRekaman",
"renameReplayText": "Mengubah nama \"${REPLAY}\" menjadi:", "renameReplayText": "Mengubah nama \"${REPLAY}\" menjadi:",
"renameText": "Ganti Nama", "renameText": "Ganti Nama",
"replayDeleteErrorText": "Kesalahan menghapus replay.", "replayDeleteErrorText": "Kesalahan menghapus rekaman.",
"replayNameText": "Nama Rekaman", "replayNameText": "Nama Rekaman",
"replayRenameErrorAlreadyExistsText": "Rekaman dengan nama tersebut sudah ada", "replayRenameErrorAlreadyExistsText": "Rekaman dengan nama tersebut sudah ada.",
"replayRenameErrorInvalidName": "Tidak dapat mengganti nama rekaman; nama tidak valid", "replayRenameErrorInvalidName": "Tidak dapat mengganti nama rekaman; nama tidak valid.",
"replayRenameErrorText": "Error mengganti nama rekaman", "replayRenameErrorText": "Error mengganti nama rekaman.",
"sharedReplaysText": "Replay Yang Dibagikan", "sharedReplaysText": "Rekaman Yang Dibagikan",
"titleText": "Tonton", "titleText": "Tonton",
"watchReplayButtonText": "Lihat\nReplay" "watchReplayButtonText": "Lihat\nReplay"
}, },

View file

@ -1702,7 +1702,8 @@
"Arabic": "Arabo", "Arabic": "Arabo",
"Belarussian": "Bielorusso", "Belarussian": "Bielorusso",
"Chinese": "Cinese Semplificato", "Chinese": "Cinese Semplificato",
"ChineseTraditional": "Cinese Tradizionale", "ChineseSimplified": "Cinese - Semplificato",
"ChineseTraditional": "Cinese - Tradizionale",
"Croatian": "Croato", "Croatian": "Croato",
"Czech": "Ceco", "Czech": "Ceco",
"Danish": "Danese", "Danish": "Danese",
@ -1726,11 +1727,15 @@
"PirateSpeak": "Piratese", "PirateSpeak": "Piratese",
"Polish": "Polacco", "Polish": "Polacco",
"Portuguese": "Portoghese", "Portuguese": "Portoghese",
"PortugueseBrazil": "Portoghese - Brasile",
"PortuguesePortugal": "Portoghese - Portogallo",
"Romanian": "Rumeno", "Romanian": "Rumeno",
"Russian": "Russo", "Russian": "Russo",
"Serbian": "Serbo", "Serbian": "Serbo",
"Slovak": "Slovacco", "Slovak": "Slovacco",
"Spanish": "Spagnolo", "Spanish": "Spagnolo",
"SpanishLatinAmerica": "Spagnolo - America Latina",
"SpanishSpain": "Spagnolo - Spagna",
"Swedish": "Svedese", "Swedish": "Svedese",
"Tamil": "Tamil", "Tamil": "Tamil",
"Thai": "Tailandese", "Thai": "Tailandese",

1989
dist/ba_data/data/languages/kazakh.json vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -380,7 +380,8 @@
"reduceWaitText": "대기 시간 단축", "reduceWaitText": "대기 시간 단축",
"slotDescriptionText": "이 슬롯은 상자를 보관할 수 있습니다.\n\n캠페인 레벨을 플레이하고,\n토너먼트에서 순위를 매기고,\n업적을 달성하여 상자를 획득하세요.", "slotDescriptionText": "이 슬롯은 상자를 보관할 수 있습니다.\n\n캠페인 레벨을 플레이하고,\n토너먼트에서 순위를 매기고,\n업적을 달성하여 상자를 획득하세요.",
"slotText": "상자 슬롯 ${NUM}", "slotText": "상자 슬롯 ${NUM}",
"slotsFullWarningText": "경고: 모든 상자 슬롯이 찼습니다.\n이 게임에서 획득한 상자는 모두 사라지게 됩니다." "slotsFullWarningText": "경고: 모든 상자 슬롯이 찼습니다.\n이 게임에서 획득한 상자는 모두 사라지게 됩니다.",
"unlocksInText": "잠금 해제"
}, },
"choosingPlayerText": "<플레이어 선택>", "choosingPlayerText": "<플레이어 선택>",
"claimText": "청구", "claimText": "청구",
@ -1380,6 +1381,7 @@
}, },
"spaceKeyText": "스페이스", "spaceKeyText": "스페이스",
"statsText": "전적", "statsText": "전적",
"stopRemindingMeText": "그만 표시하기",
"storagePermissionAccessText": "이 행위는 저장소 접근이 필요합니다.", "storagePermissionAccessText": "이 행위는 저장소 접근이 필요합니다.",
"store": { "store": {
"alreadyOwnText": "이미 ${NAME}(을)를 소유 중입니다!", "alreadyOwnText": "이미 ${NAME}(을)를 소유 중입니다!",
@ -1454,6 +1456,7 @@
"getTokensText": "토큰 얻기", "getTokensText": "토큰 얻기",
"notEnoughTokensText": "토큰이 부족합니다!", "notEnoughTokensText": "토큰이 부족합니다!",
"numTokensText": "${COUNT}개 토큰", "numTokensText": "${COUNT}개 토큰",
"openNowDescriptionText": "지금 상자를 열 수 있는\n토큰이 충분합니다.\n기다릴 필요가 없습니다.",
"shinyNewCurrencyText": "BombSquad의 반짝이는 새로운 화폐.", "shinyNewCurrencyText": "BombSquad의 반짝이는 새로운 화폐.",
"tokenPack1Text": "작은 토큰 팩", "tokenPack1Text": "작은 토큰 팩",
"tokenPack2Text": "중간 토큰 팩", "tokenPack2Text": "중간 토큰 팩",
@ -1617,7 +1620,8 @@
"Arabic": "아랍어", "Arabic": "아랍어",
"Belarussian": "벨로루시어", "Belarussian": "벨로루시어",
"Chinese": "중국어 간체", "Chinese": "중국어 간체",
"ChineseTraditional": "중국어 번체", "ChineseSimplified": "중국어 간체자",
"ChineseTraditional": "중국어 번체자",
"Croatian": "크로아티아어", "Croatian": "크로아티아어",
"Czech": "체코어", "Czech": "체코어",
"Danish": "덴마크어", "Danish": "덴마크어",
@ -1641,11 +1645,15 @@
"PirateSpeak": "해적의 말", "PirateSpeak": "해적의 말",
"Polish": "폴란드어", "Polish": "폴란드어",
"Portuguese": "포르투갈어", "Portuguese": "포르투갈어",
"PortugueseBrazil": "브라질 포르투갈어",
"PortuguesePortugal": "포르투갈어",
"Romanian": "루마니아어", "Romanian": "루마니아어",
"Russian": "러시아어", "Russian": "러시아어",
"Serbian": "세르비아어", "Serbian": "세르비아어",
"Slovak": "슬로바키아어", "Slovak": "슬로바키아어",
"Spanish": "스페인어", "Spanish": "스페인어",
"SpanishLatinAmerica": "라틴 아메리카 스페인어",
"SpanishSpain": "스페인어",
"Swedish": "스웨덴어", "Swedish": "스웨덴어",
"Tamil": "타밀어", "Tamil": "타밀어",
"Thai": "태국어", "Thai": "태국어",
@ -1751,6 +1759,7 @@
"Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "이 계정에서 ${ACCOUNT} 의 연동을 해제하시겠습니까?\n${ACCOUNT} 에 있는 모든 데이터가 초기화됩니다.\n(일부 상황에서 도전과제 빼고는)", "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "이 계정에서 ${ACCOUNT} 의 연동을 해제하시겠습니까?\n${ACCOUNT} 에 있는 모든 데이터가 초기화됩니다.\n(일부 상황에서 도전과제 빼고는)",
"WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "경고: 당신 계정에 해킹 관련 경고가 전해졌습니다.\n해킹 중인 걸로 밝혀진 계정은 즉시 차단됩니다. 제발 게임만은 공정하게 합시다.", "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "경고: 당신 계정에 해킹 관련 경고가 전해졌습니다.\n해킹 중인 걸로 밝혀진 계정은 즉시 차단됩니다. 제발 게임만은 공정하게 합시다.",
"Wait reduced!": "대기 시간이 단축됐습니다!", "Wait reduced!": "대기 시간이 단축됐습니다!",
"Warning: This version of the game is limited to old account data; things may appear missing or out of date.\nPlease upgrade to a newer version of the game to see your latest account data.": "경고: 이 버전의 게임은 이전 계정 데이터만 지원합니다. 일부 데이터가 누락되었거나 오래된 것처럼 보일 수 있습니다.\n최신 계정 데이터를 확인하려면 최신 버전의 게임으로 업그레이드하세요.",
"Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "귀하의 기기를 이 계정에 연동하시겠습니까?\n\n귀하의 기기 계정: ${ACCOUNT1}\n이 계정: ${ACCOUNT2}\n\n이로써 기존 진행 상황을 유지할 수 있습니다.\n경고: 이 작업은 취소할 수 없습니다!", "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "귀하의 기기를 이 계정에 연동하시겠습니까?\n\n귀하의 기기 계정: ${ACCOUNT1}\n이 계정: ${ACCOUNT2}\n\n이로써 기존 진행 상황을 유지할 수 있습니다.\n경고: 이 작업은 취소할 수 없습니다!",
"You already own this!": "이미 소유 중입니다!", "You already own this!": "이미 소유 중입니다!",
"You can join in ${COUNT} seconds.": "${COUNT} 초 후에 참가할 수 있습니다.", "You can join in ${COUNT} seconds.": "${COUNT} 초 후에 참가할 수 있습니다.",
@ -1921,6 +1930,7 @@
"twoKillText": "더블 킬!", "twoKillText": "더블 킬!",
"uiScaleText": "Ui 크기", "uiScaleText": "Ui 크기",
"unavailableText": "이용할 수 없음", "unavailableText": "이용할 수 없음",
"unclaimedPrizesText": "청구되지 않은 상품이 있습니다!",
"unconfiguredControllerDetectedText": "구성되지 않은 컨트롤러가 검색됨:", "unconfiguredControllerDetectedText": "구성되지 않은 컨트롤러가 검색됨:",
"unlockThisInTheStoreText": "상점에서 잠금 해제해야 합니다.", "unlockThisInTheStoreText": "상점에서 잠금 해제해야 합니다.",
"unlockThisProfilesText": "${NUM}개 이상의 프로필을 만들기 위해, 다음 사항이 필요합니다. :", "unlockThisProfilesText": "${NUM}개 이상의 프로필을 만들기 위해, 다음 사항이 필요합니다. :",

View file

@ -166,10 +166,10 @@
"name": "جادوگر ${LEVEL}" "name": "جادوگر ${LEVEL}"
}, },
"Precision Bombing": { "Precision Bombing": {
"description": "بدون گرفتن هیچ قدرتی برنده شو", "description": "بدون گرفتن هیچ نیروزایی برنده شو",
"descriptionComplete": "بدون گرفتن هیچ قدرتی برنده شدی", "descriptionComplete": "بدون گرفتن هیچ نیروزایی برنده شدی",
"descriptionFull": "${LEVEL} رو بدون گرفتن هیچ قدرتی برنده شو", "descriptionFull": "${LEVEL} رو بدون گرفتن هیچ نیروزایی برنده شو",
"descriptionFullComplete": "${LEVEL} رو بدون گرفتن هیچ قدرتی برنده شدی", "descriptionFullComplete": "${LEVEL} رو بدون گرفتن هیچ نیروزایی برنده شدی",
"name": "بمب‌باران دقیق" "name": "بمب‌باران دقیق"
}, },
"Pro Boxer": { "Pro Boxer": {
@ -184,7 +184,7 @@
"descriptionComplete": "بدون اینکه بزاری حریف امتیاز بگیره، برنده شدی", "descriptionComplete": "بدون اینکه بزاری حریف امتیاز بگیره، برنده شدی",
"descriptionFull": "در ${LEVEL} بدون اینکه بزاری حریف امتیاز بگیره، برنده شو", "descriptionFull": "در ${LEVEL} بدون اینکه بزاری حریف امتیاز بگیره، برنده شو",
"descriptionFullComplete": "در ${LEVEL} بدون اینکه بزاری حریف امتیاز بگیره، برنده شدی", "descriptionFullComplete": "در ${LEVEL} بدون اینکه بزاری حریف امتیاز بگیره، برنده شدی",
"name": وازه‌بسته در ${LEVEL}" "name": روازه‌بسته در ${LEVEL}"
}, },
"Pro Football Victory": { "Pro Football Victory": {
"description": "برنده شو", "description": "برنده شو",
@ -377,10 +377,10 @@
"chests": { "chests": {
"prizeOddsText": "جایزه خفن", "prizeOddsText": "جایزه خفن",
"reduceWaitText": "کاهش انتظار", "reduceWaitText": "کاهش انتظار",
"slotDescriptionText": "این مکان می تواند یک صندوق را نگه دارد\n\nبا بازی سطوح کمپین،\nقرار گرفتن در مسابقات و تکمیل\nدستاوردها صندوق بدست آورید", "slotDescriptionText": "این شکاف می‌تواند یک صندوق نگه دارد.\n\nبا بازی در مرحله‌های کمپین، رتبه‌گیری در\nناوردگان و تکمیل دستاوردها صندوق\nبدست آورید.",
"slotText": "محل صندوق ${NUM}", "slotText": "${NUM} شکاف صندوق",
"slotsFullWarningText": "هشدار: تمام محل های صندوق شما پر است.\n هر صندوق ای که در این بازی به دست آورید از بین خواهد رفت", "slotsFullWarningText": "هشدار: تمام محل های صندوق شما پر است.\n هر صندوق ای که در این بازی به دست آورید از بین خواهد رفت",
"unlocksInText": "باز می کند" "unlocksInText": "باز می‌شود در عرض"
}, },
"choosingPlayerText": "<انتخاب بازیکن>", "choosingPlayerText": "<انتخاب بازیکن>",
"claimText": "دریافت", "claimText": "دریافت",
@ -396,11 +396,11 @@
"ps3Text": "PS3 دسته", "ps3Text": "PS3 دسته",
"titleText": "دسته‌ها", "titleText": "دسته‌ها",
"wiimotesText": "ها Wiimote", "wiimotesText": "ها Wiimote",
"xbox360Text": "Xbox 360 دسته" "xbox360Text": "Xbox 360 دسته‌ی"
}, },
"configGamepadSelectWindow": { "configGamepadSelectWindow": {
"androidNoteText": "تذکر: پشتیبانی از دسته با توجه به دستگاه و نسخه ی اندرویدش متفاوت است", "androidNoteText": "تذکر: پشتیبانی از دسته با توجه به دستگاه و نسخه‌ی اندروید متفاوت است.",
"pressAnyButtonText": "یکی از دکمه های دسته ای که میخواهید\n تنظیم کنید را فشار دهید", "pressAnyButtonText": "یکی از دکمه‌های دسته‌ای که می‌خواهید\nتنظیم کنید را فشار دهید...",
"titleText": "تنظیم دسته" "titleText": "تنظیم دسته"
}, },
"configGamepadWindow": { "configGamepadWindow": {
@ -453,15 +453,15 @@
"keyboard2NoteText": "تذکر:بیشتر کیبوردها فقط چند دکمه را همزمان میپذیرند\nپس داشتن یه کیبورد دیگه ممکنه مفید باشه\nاگر کیبورد متصل و جدای دیگه هم برای استفاده باشه\nتوجه کنید که هنوز هم لازمه دکمه های خاصی رو \nبرای دو کیبورد تنظیم کنید" "keyboard2NoteText": "تذکر:بیشتر کیبوردها فقط چند دکمه را همزمان میپذیرند\nپس داشتن یه کیبورد دیگه ممکنه مفید باشه\nاگر کیبورد متصل و جدای دیگه هم برای استفاده باشه\nتوجه کنید که هنوز هم لازمه دکمه های خاصی رو \nبرای دو کیبورد تنظیم کنید"
}, },
"configTouchscreenWindow": { "configTouchscreenWindow": {
"actionControlScaleText": "اندازه ی دکمه ها", "actionControlScaleText": "اندازه‌ی دکمه‌ها",
"actionsText": "اعمال", "actionsText": "اعمال",
"buttonsText": "کلید ها", "buttonsText": "دکمه",
"dragControlsText": "< دکمه‌ها را بکشید و موقعیتشان را تعیین کنید >", "dragControlsText": "< دکمه‌ها را بکشید و موقعیتشان را تعیین کنید >",
"joystickText": "دکمه ی حرکت", "joystickText": "اهرمک",
"movementControlScaleText": "اندازه ی دکمه ی حرکت", "movementControlScaleText": "اندازه‌ی دکمه‌ی حرکت",
"movementText": "حرکت", "movementText": "حرکت",
"resetText": "بازنشانی", "resetText": "بازنشانی",
"swipeControlsHiddenText": "مخفی کردن دکمه ی حرکت", "swipeControlsHiddenText": "پنهان کردن آیکون حرکت جاروبی",
"swipeInfoText": "کمی طول میکشد به این نوع حرکت عادت کنید\nولی راحت باشید و بدون نگاه کردن به آن بازی کنید", "swipeInfoText": "کمی طول میکشد به این نوع حرکت عادت کنید\nولی راحت باشید و بدون نگاه کردن به آن بازی کنید",
"swipeText": "حرکت جاروبی", "swipeText": "حرکت جاروبی",
"titleText": "پیکربندی صفحه لمسی" "titleText": "پیکربندی صفحه لمسی"
@ -488,7 +488,7 @@
"activenessAllTimeInfoText": "بر روی رده‌بندی کلی اِعمال نمی‌شود.", "activenessAllTimeInfoText": "بر روی رده‌بندی کلی اِعمال نمی‌شود.",
"activenessInfoText": "این افزاینده در روزهایی که بازی می‌کنید افزایش می‌یابد\nو در روزهایی که بازی نمی‌کنید کاهش می‌یابد.", "activenessInfoText": "این افزاینده در روزهایی که بازی می‌کنید افزایش می‌یابد\nو در روزهایی که بازی نمی‌کنید کاهش می‌یابد.",
"activityText": "فعالیت", "activityText": "فعالیت",
"campaignText": "پیشروی", "campaignText": "پیشروی",
"challengesInfoText": "برای کامل کردن مینی‌بازی‌ها جایزه بگیرید.\n\nهرگاه چالشی را انجام می‌دهید، جایزه‌ها و\nسختی مراحل افزایش می‌یابد و هرگاه چالشی\nباطل شود یا به هدر رود، کاهش می‌یابد.", "challengesInfoText": "برای کامل کردن مینی‌بازی‌ها جایزه بگیرید.\n\nهرگاه چالشی را انجام می‌دهید، جایزه‌ها و\nسختی مراحل افزایش می‌یابد و هرگاه چالشی\nباطل شود یا به هدر رود، کاهش می‌یابد.",
"challengesText": "چالش‌ها", "challengesText": "چالش‌ها",
"currentBestText": "بهترین امتیاز کنونی", "currentBestText": "بهترین امتیاز کنونی",
@ -517,9 +517,9 @@
"timeRemainingText": "زمان باقی‌مانده", "timeRemainingText": "زمان باقی‌مانده",
"toRankedText": "تا رتبه‌بندی شوید", "toRankedText": "تا رتبه‌بندی شوید",
"totalText": "⁦مجموع", "totalText": "⁦مجموع",
"tournamentInfoText": "بر سر امتیاز بیشتر با بازیکنان در\nلیگ خود رقابت کنید.\n\nهنگامی که زمان مسابقه تمام شود، جایزه به\nنفرات برتر با امتیازهای بالا داده می‌شود.", "tournamentInfoText": "بر سر امتیاز بیشتر با بازیکنان در\nلیگ خود رقابت کنید.\n\nهنگامی که زمان ناورد تمام شود، جایزه به\nنفرات برتر با امتیازهای بالا داده می‌شود.",
"welcome1Text": "خوش آمدید. شما می‌توانید ${LEAGUE} به لیگ\nبا گرفتن امتیاز، کامل کردن دستاوردها یا گرفتن جام\n.در مسابقات رتبهٔ خود را بهبود بخشید", "welcome1Text": "خوش آمدید. شما می‌توانید ${LEAGUE} به لیگ\nبا گرفتن امتیاز، کامل کردن دستاوردها یا گرفتن جام\n.در ناوردگان، رتبهٔ خود را بهبود بخشید",
"welcome2Text": "همچنین می‌توانید از راه‌های مشابه بلیت جمع‌آوری کنید.\nبلیتها می‌توانند برای باز کردن بازیکنان جدید، نقشه‌ها، مینی‌بازی‌ها یا برای ورود در مسابقه‌ها و موارد\nبیشتر مورد استفاده قرار گیرند.", "welcome2Text": "همچنین می‌توانید از راه‌های مشابه بلیت جمع‌آوری کنید.\nبلیتها می‌توانند برای بازگشایی شخصیت‌ها، نقشه‌ها و مینی‌بازی‌های\nجدید یا برای ورود به ناوردگان و موارد بیشتر، به‌کار می‌روند.",
"yourPowerRankingText": "رتبه‌بندی قدرت شما:" "yourPowerRankingText": "رتبه‌بندی قدرت شما:"
}, },
"copyConfirmText": "در حافظه کلیپ بورد شما کپی شد.", "copyConfirmText": "در حافظه کلیپ بورد شما کپی شد.",
@ -630,27 +630,27 @@
"cantEditDefaultText": ".نمیتوانید صدای پیشفرض رو دست کاری کنید. آن را کپی کنید یا یه جدید بسازید", "cantEditDefaultText": ".نمیتوانید صدای پیشفرض رو دست کاری کنید. آن را کپی کنید یا یه جدید بسازید",
"cantOverwriteDefaultText": "نمیشه صدای پیشفرض رو بازنویسی کرد", "cantOverwriteDefaultText": "نمیشه صدای پیشفرض رو بازنویسی کرد",
"cantSaveAlreadyExistsText": "یک صدا با همین نام وجود داره", "cantSaveAlreadyExistsText": "یک صدا با همین نام وجود داره",
"defaultGameMusicText": "<موسیقی پیش فرض بازی>", "defaultGameMusicText": "<موسیقی پیش فرض بازی>",
"defaultSoundtrackNameText": "صدای پیش فرض", "defaultSoundtrackNameText": "صدای پیشفرض",
"deleteConfirmText": "حذف صدا با نام:\n\n'${NAME}'?", "deleteConfirmText": "حذف صدا با نام:\n\n'${NAME}'?",
"deleteText": "حذف\nصدا", "deleteText": "حذف\nصدا",
"duplicateText": "ایجاد کپی\nاز صدا", "duplicateText": "ایجاد کپی\nاز صدا",
"editSoundtrackText": "ویرایشگر صدا", "editSoundtrackText": "ویرایشگر صدا",
"editText": "ویرایش\nصدا", "editText": "ویرایش\nصدا",
"fetchingITunesText": "گرفتن صدا از لیست پخش برنامه", "fetchingITunesText": "گرفتن صدا از لیست پخش برنامه",
"musicVolumeZeroWarning": "هشدار:درجه ی صدای موسیقی روی صفر است", "musicVolumeZeroWarning": "هشدار: درجه صدای موسیقی روی 0 است",
"nameText": "نام", "nameText": "نام",
"newSoundtrackNameText": "${COUNT} صدای من", "newSoundtrackNameText": "${COUNT} صدای من",
"newSoundtrackText": "صدای جدید:", "newSoundtrackText": "صدای جدید:",
"newText": "صدای\nجدید", "newText": "صدای\nجدید",
"selectAPlaylistText": "انتخاب یه لیست", "selectAPlaylistText": "انتخاب یه لیست",
"selectASourceText": "منبع موسیقی", "selectASourceText": "منبع موسیقی",
"testText": "آزمایشی", "testText": "آزمایش",
"titleText": "صداهای پس زمینه", "titleText": "موسیقی‌متن‌ها",
"useDefaultGameMusicText": "موسیقی پیشفرض بازی", "useDefaultGameMusicText": "موسیقی پیشفرض بازی",
"useITunesPlaylistText": "لیست موسیقی برنامه", "useITunesPlaylistText": "لیست موسیقی برنامه",
"useMusicFileText": "(...و mp3)فایل موسیقی", "useMusicFileText": "(...و mp3) فایل موسیقی",
"useMusicFolderText": "پوشه ی فایل های موسیقی" "useMusicFolderText": "پوشه‌ی فایل‌های موسیقی"
}, },
"editText": "ویرایش", "editText": "ویرایش",
"enabledText": "فعال", "enabledText": "فعال",
@ -713,7 +713,7 @@
"gamesToText": "${LOSECOUNT} بازی به ${WINCOUNT}", "gamesToText": "${LOSECOUNT} بازی به ${WINCOUNT}",
"gatherWindow": { "gatherWindow": {
"aboutDescriptionLocalMultiplayerExtraText": "فراموش نکنید: هردستگاه در یک گروه میتواند بیشتر\n.از یک بازیکن داشته باشد اگر به اندازه ی کافی دسته دارید", "aboutDescriptionLocalMultiplayerExtraText": "فراموش نکنید: هردستگاه در یک گروه میتواند بیشتر\n.از یک بازیکن داشته باشد اگر به اندازه ی کافی دسته دارید",
"aboutDescriptionText": ".از این زبانه‌ها برای تشکیل یک پارتی استفاده کنید\n\nپارتی به شما این امکان را می‌دهد که بازی‌ها و مسابقات\n.را با دوستانتان بر روی گوشی‌های متفاوت بازی کنید\n\nدر گوشه‌ی بالای سمت راست استفاده کنید ${PARTY} از دکمه‌ی\n.تا با پارتی چت و تعامل کنید\n(⁦را هنگامی که در منو هستید فشار دهید ${BUTTON} با دسته، دکمه‌ی)", "aboutDescriptionText": ".از این زبانه‌ها برای تشکیل یک پارتی استفاده کنید\n\nپارتی به شما این امکان را می‌دهد که بازی‌ها و ناوردگان\n.را با دوستانتان بر روی گوشی‌های متفاوت بازی کنید\n\nدر گوشه‌ی بالای سمت راست استفاده کنید ${PARTY} از دکمه‌ی\n.تا با پارتی چت و تعامل کنید\n(⁦را هنگامی که در منو هستید فشار دهید ${BUTTON} با دسته، دکمه‌ی)",
"aboutText": "درباره", "aboutText": "درباره",
"addressFetchErrorText": "<خطا در اتصال به آدرس>", "addressFetchErrorText": "<خطا در اتصال به آدرس>",
"appInviteMessageText": "${APP_NAME}بلیت فرستاده در برنامه ی ${COUNT}برای شما ${NAME}", "appInviteMessageText": "${APP_NAME}بلیت فرستاده در برنامه ی ${COUNT}برای شما ${NAME}",
@ -786,7 +786,7 @@
"partyInviteText": "شما را دعوت کرده${NAME} \nتا به گروهشان ملحق شوید", "partyInviteText": "شما را دعوت کرده${NAME} \nتا به گروهشان ملحق شوید",
"partyNameText": "نام پارتی", "partyNameText": "نام پارتی",
"partyServerRunningText": ".سرور پارتی شما در حال اجراست", "partyServerRunningText": ".سرور پارتی شما در حال اجراست",
"partySizeText": "اندازه دسته", "partySizeText": "اندازه‌ی پارتی",
"partyStatusCheckingText": "در حال چک کردن وضعیت...", "partyStatusCheckingText": "در حال چک کردن وضعیت...",
"partyStatusJoinableText": "گروه شما حالا دیگه از طریق اینترنت قابل اتصال برای بقیه است.", "partyStatusJoinableText": "گروه شما حالا دیگه از طریق اینترنت قابل اتصال برای بقیه است.",
"partyStatusNoConnectionText": "عدم توانایی برقراری ارتباط با سرور", "partyStatusNoConnectionText": "عدم توانایی برقراری ارتباط با سرور",
@ -794,7 +794,7 @@
"partyStatusNotPublicText": "پارتی شما عمومی نیست", "partyStatusNotPublicText": "پارتی شما عمومی نیست",
"pingText": "پینگ", "pingText": "پینگ",
"portText": "درگاه", "portText": "درگاه",
"privatePartyCloudDescriptionText": ".گروه‌های خصوصی بر روی سرورهای ابری اختصاصی اجرا می شوند; و نیازی به پیکربندی روتر/مودم نیست", "privatePartyCloudDescriptionText": ".پارتی‌های خصوصی روی سرورهای ابری اختصاصی اجرا می‌شوند؛ نیازی به پیکربندی روتر/مودم نیست",
"privatePartyHostText": "میزبانی پارتی خصوصی", "privatePartyHostText": "میزبانی پارتی خصوصی",
"privatePartyJoinText": "پیوستن به سرور خصوصی", "privatePartyJoinText": "پیوستن به سرور خصوصی",
"privateText": "خصوصی", "privateText": "خصوصی",
@ -835,7 +835,7 @@
"titleText": "بلیط بگیرید", "titleText": "بلیط بگیرید",
"unavailableLinkAccountText": ".ببخشید،خرید به وسیله ی این دستگاه در دسترس نمیباشد\nمیتوانید حسابتان بر روی این دستگاه را به حسابی در دستگاهی\n.دیگر متصل کنید و آنجا خرید خود را انجام دهید", "unavailableLinkAccountText": ".ببخشید،خرید به وسیله ی این دستگاه در دسترس نمیباشد\nمیتوانید حسابتان بر روی این دستگاه را به حسابی در دستگاهی\n.دیگر متصل کنید و آنجا خرید خود را انجام دهید",
"unavailableTemporarilyText": "در حال حاضر در دسترس نمیباشد. لطفا بعدا دوباره امتحان کنید", "unavailableTemporarilyText": "در حال حاضر در دسترس نمیباشد. لطفا بعدا دوباره امتحان کنید",
"unavailableText": "متاسفانه , دردسترس نیست", "unavailableText": "متاسفیم، این مورد در دسترس نیست.",
"versionTooOldText": "متاسفم،این ورژن بازی خیلی قدیمی است. لطفا بازی را آپدیت کنید", "versionTooOldText": "متاسفم،این ورژن بازی خیلی قدیمی است. لطفا بازی را آپدیت کنید",
"youHaveShortText": "دارید ${COUNT} شما", "youHaveShortText": "دارید ${COUNT} شما",
"youHaveText": ".بلیط دارید ${COUNT} شما" "youHaveText": ".بلیط دارید ${COUNT} شما"
@ -952,8 +952,8 @@
"incompatibleVersionHostText": "میزبان در حال اجرا از نسخه دیگری از این بازی است.\nمطمئن شوید که شما هر دو تا بروز هستید و دوباره امتحان کنید.", "incompatibleVersionHostText": "میزبان در حال اجرا از نسخه دیگری از این بازی است.\nمطمئن شوید که شما هر دو تا بروز هستید و دوباره امتحان کنید.",
"incompatibleVersionPlayerText": "${NAME} در حال اجرا از نسخه دیگری از این بازی است.\nمطمئن شوید که شما هر دو تا بروز هستید و دوباره امتحان کنید.", "incompatibleVersionPlayerText": "${NAME} در حال اجرا از نسخه دیگری از این بازی است.\nمطمئن شوید که شما هر دو تا بروز هستید و دوباره امتحان کنید.",
"invalidAddressErrorText": "خطا: آدرس در دسترس نیست", "invalidAddressErrorText": "خطا: آدرس در دسترس نیست",
"invalidNameErrorText": "نام نا معتبر", "invalidNameErrorText": "خطا: نام نامعتبر.",
"invalidPortErrorText": "مشکل:درگاه بی اعتبار", "invalidPortErrorText": "خطا: درگاه نامعتبر",
"invitationSentText": "دعوت نامه ارسال شد.", "invitationSentText": "دعوت نامه ارسال شد.",
"invitationsSentText": "دعوت نامه ارسال شد ${COUNT}", "invitationsSentText": "دعوت نامه ارسال شد ${COUNT}",
"joinedPartyInstructionsText": "فردی به گروه شما پیوسته\nبرید به بازی و بازی را شروع کنید", "joinedPartyInstructionsText": "فردی به گروه شما پیوسته\nبرید به بازی و بازی را شروع کنید",
@ -967,7 +967,7 @@
"playerJoinedPartyText": "به گروه بازی پیوست ${NAME}", "playerJoinedPartyText": "به گروه بازی پیوست ${NAME}",
"playerLeftPartyText": ".⁦پارتی رو ترک کرد ${NAME}", "playerLeftPartyText": ".⁦پارتی رو ترک کرد ${NAME}",
"rejectingInviteAlreadyInPartyText": "رد کردن دعوت (already in a party)", "rejectingInviteAlreadyInPartyText": "رد کردن دعوت (already in a party)",
"serverRestartingText": "سرور در حال شروع مجدد است. لطفا چند لحظه دیگر مجددا متصل شوید", "serverRestartingText": "سرور در حال بازراه‌اندازی است. لطفا در لحظه‌ای دیگر دوباره بپیوندید...",
"serverShuttingDownText": ".سرور درحال بسته شدن است", "serverShuttingDownText": ".سرور درحال بسته شدن است",
"signInErrorText": "خطای ورود به سیستم", "signInErrorText": "خطای ورود به سیستم",
"signInNoConnectionText": "ورود ناموفق بود. اتصال به اینترنت برقراره؟", "signInNoConnectionText": "ورود ناموفق بود. اتصال به اینترنت برقراره؟",
@ -1023,14 +1023,14 @@
"leagueRankText": "رتبه لیگ", "leagueRankText": "رتبه لیگ",
"leagueText": "لیگ", "leagueText": "لیگ",
"rankInLeagueText": "#${RANK}, ${NAME} League${SUFFIX}", "rankInLeagueText": "#${RANK}, ${NAME} League${SUFFIX}",
"seasonEndedDaysAgoText": ".روز پیش پایان یافت ${NUMBER} فصل", "seasonEndedDaysAgoText": ".روز پیش پایان یافت ${NUMBER} فصل،",
"seasonEndsDaysText": ".روز دیگر پایان می‌یابد ${NUMBER} فصل", "seasonEndsDaysText": ".روز دیگر پایان می‌یابد ${NUMBER} فصل،",
"seasonEndsHoursText": ".ساعت دیگر پایان می‌یابد ${NUMBER} فصل", "seasonEndsHoursText": ".ساعت دیگر پایان می‌یابد ${NUMBER} فصل،",
"seasonEndsMinutesText": ".دقیقه‌ی دیگر پایان می‌یابد ${NUMBER} فصل", "seasonEndsMinutesText": ".دقیقه‌ی دیگر پایان می‌یابد ${NUMBER} فصل،",
"seasonText": "${NUMBER} فصل", "seasonText": "${NUMBER} فصل",
"tournamentLeagueText": ".برسید ${NAME} برای ورود به این مسابقه، باید به لیگ", "tournamentLeagueText": "برای ورود به این ناورد، باید به لیگ ${NAME} برسید.",
"trophyCountsResetText": ".جوایز در فصل بعد بازنشانی می‌شوند", "trophyCountsResetText": ".جایزهها در فصل بعد بازنشانی می‌شوند",
"upToDateBonusDescriptionText": "بازیکنانی نسخه اخیر را اجرا می کنند بازی در اینجا جایزه دریافت می کند ${PERCENT}%", "upToDateBonusDescriptionText": "بازیکنانی که نسخه‌ی اخیری از بازی را اجرا می‌کنند،\n.‎امتیاز اضافی در اینجا دریافت می‌کنند ${PERCENT}%",
"upToDateBonusText": "پاداش به‌روز بودن" "upToDateBonusText": "پاداش به‌روز بودن"
}, },
"learnMoreText": "بیشتر بدانید", "learnMoreText": "بیشتر بدانید",
@ -1107,11 +1107,11 @@
"noExternalStorageErrorText": "محل ذخیره سازی در این دستگاه یافت نشد", "noExternalStorageErrorText": "محل ذخیره سازی در این دستگاه یافت نشد",
"noGameCircleText": "GameCircleخطا: وارد نشدید به", "noGameCircleText": "GameCircleخطا: وارد نشدید به",
"noMessagesText": "هیچ پیامی فعلا نیست", "noMessagesText": "هیچ پیامی فعلا نیست",
"noPluginsInstalledText": "متاسفانه افزونه ها نصب نشده", "noPluginsInstalledText": "افزونه‌ای نصب نیست",
"noScoresYetText": "هیچ امتیازی نیست", "noScoresYetText": "هیچ امتیازی نیست",
"noServersFoundText": "سروری یافت نشد.", "noServersFoundText": "سروری یافت نشد.",
"noThanksText": "نه مرسی", "noThanksText": "نه مرسی",
"noTournamentsInTestBuildText": ".هشدار: امتیازات مسابقه از این نسخهٔ آزمایشی نادیده گرفته می‌شوند", "noTournamentsInTestBuildText": ".هشدار: امتیازهای ناورد از این نسخهٔ آزمایشی نادیده گرفته می‌شوند",
"noValidMapsErrorText": "هیچ نقشه معتبری برای این نوع بازی یافت نشد.", "noValidMapsErrorText": "هیچ نقشه معتبری برای این نوع بازی یافت نشد.",
"notEnoughPlayersRemainingText": "بازیکنان باقیمانده کافی نیستند خارج بشید و دوباره یه بازی جدید رو شروع کنید", "notEnoughPlayersRemainingText": "بازیکنان باقیمانده کافی نیستند خارج بشید و دوباره یه بازی جدید رو شروع کنید",
"notEnoughPlayersText": "!بازیکن نیاز دارید ${COUNT} برای شروع بازی حداقل به", "notEnoughPlayersText": "!بازیکن نیاز دارید ${COUNT} برای شروع بازی حداقل به",
@ -1153,13 +1153,13 @@
"singlePlayerCoopText": "بازی تک‌نفره / چندنفره", "singlePlayerCoopText": "بازی تک‌نفره / چندنفره",
"teamsText": "بازی تیمی" "teamsText": "بازی تیمی"
}, },
"playText": "شروع بازی", "playText": "بازی",
"playWindow": { "playWindow": {
"oneToFourPlayersText": "۱ تا ۴ بازیکن", "oneToFourPlayersText": "۴-۱ بازیکن",
"titleText": "شروع بازی", "titleText": "بازی",
"twoToEightPlayersText": "۲ تا ۸ بازیکن" "twoToEightPlayersText": "۸ بازیکن"
}, },
"playerCountAbbreviatedText": "(⁦نفره ${COUNT})", "playerCountAbbreviatedText": "${COUNT}p",
"playerDelayedJoinText": ".⁦در دور بعد وارد می‌شود ${PLAYER}", "playerDelayedJoinText": ".⁦در دور بعد وارد می‌شود ${PLAYER}",
"playerInfoText": "اطلاعات بازیکن", "playerInfoText": "اطلاعات بازیکن",
"playerLeftText": ".⁦بازی را ترک کرد ${PLAYER}", "playerLeftText": ".⁦بازی را ترک کرد ${PLAYER}",
@ -1178,7 +1178,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": "${ERROR} :«${PLUGIN}» خطا در بارگیری دسته‌بندی افزونهٔ", "pluginClassLoadErrorText": "${ERROR} :«${PLUGIN}» خطا در بارگیری دسته‌بندی افزونهٔ",
"pluginInitErrorText": "${ERROR} :«${PLUGIN}» خطا در راه‌اندازی افزونهٔ", "pluginInitErrorText": "${ERROR} :«${PLUGIN}» خطا در راه‌اندازی افزونهٔ",
@ -1187,7 +1187,7 @@
"pluginsDetectedText": "افزونه(ها)ی جدید شناسایی شد. آن‌ها را در تنظیمات فعال، یا پیکربندی کنید.", "pluginsDetectedText": "افزونه(ها)ی جدید شناسایی شد. آن‌ها را در تنظیمات فعال، یا پیکربندی کنید.",
"pluginsDisableAllText": "غیرفعال کردن همه‌ی افزونه‌ها", "pluginsDisableAllText": "غیرفعال کردن همه‌ی افزونه‌ها",
"pluginsEnableAllText": "فعال کردن همه‌ی افزونه‌ها", "pluginsEnableAllText": "فعال کردن همه‌ی افزونه‌ها",
"pluginsRemovedText": "${NUM} افزونه دیگر یافت نمی‌شود.", "pluginsRemovedText": ".⁦افزونه دیگر یافت نمی‌شود ${NUM}",
"pluginsText": "افزونه‌ها", "pluginsText": "افزونه‌ها",
"practiceText": "تمرین", "practiceText": "تمرین",
"pressAnyButtonPlayAgainText": "فشردن هر دکمه‌ای برای بازی دوباره…", "pressAnyButtonPlayAgainText": "فشردن هر دکمه‌ای برای بازی دوباره…",
@ -1260,7 +1260,7 @@
"version_mismatch": "عدم تطابق نسخه‌ها.\nاطمینان حاصل کنید که بمب‌اسکواد و دسته‌مجازی\nآخرین نسخه باشند و دوباره امتحان کنید." "version_mismatch": "عدم تطابق نسخه‌ها.\nاطمینان حاصل کنید که بمب‌اسکواد و دسته‌مجازی\nآخرین نسخه باشند و دوباره امتحان کنید."
}, },
"removeInGameAdsText": "بازی را در فروشگاه بخرید تا تبلیغات حذف شوند «${PRO}» نسخهٔ", "removeInGameAdsText": "بازی را در فروشگاه بخرید تا تبلیغات حذف شوند «${PRO}» نسخهٔ",
"removeInGameAdsTokenPurchaseText": "پیشنهاد زمان محدود: هر بسته توکن را برای حذف تبلیغات درون بازی خریداری کنید", "removeInGameAdsTokenPurchaseText": "پیشنهاد زمان‌محدود: بسته توکنی را برای حذف تبلیغات درون بازی خریداری کنید",
"renameText": "تغییر نام", "renameText": "تغییر نام",
"replayEndText": "پایان بازپخش", "replayEndText": "پایان بازپخش",
"replayNameDefaultText": "بازپخش بازی اخیر", "replayNameDefaultText": "بازپخش بازی اخیر",
@ -1281,7 +1281,7 @@
"revertText": "بازگشت", "revertText": "بازگشت",
"runText": "دویدن", "runText": "دویدن",
"saveText": "ذخیره", "saveText": "ذخیره",
"scanScriptsErrorText": "اسکریپت ها در حال بررسی خطا ها است؛ برای جزئیات لاگ را ببینید", "scanScriptsErrorText": "خطا هنگام اسکن اسکریپت‌ها. برای جزییات لاگ را ببینید.",
"scanScriptsMultipleModulesNeedUpdatesText": ".⁦به‌روزرسانی شوند api ${API} ⁦ماژول دیگر باید برای ${NUM} ⁦و ${PATH}", "scanScriptsMultipleModulesNeedUpdatesText": ".⁦به‌روزرسانی شوند api ${API} ⁦ماژول دیگر باید برای ${NUM} ⁦و ${PATH}",
"scanScriptsSingleModuleNeedsUpdatesText": "${PATH} باید برای api ${API} به‌روزرسانی شود.", "scanScriptsSingleModuleNeedsUpdatesText": "${PATH} باید برای api ${API} به‌روزرسانی شود.",
"scoreChallengesText": "امتیاز چالش", "scoreChallengesText": "امتیاز چالش",
@ -1309,7 +1309,7 @@
"titleText": "تنظیمات" "titleText": "تنظیمات"
}, },
"settingsWindowAdvanced": { "settingsWindowAdvanced": {
"alwaysUseInternalKeyboardDescriptionText": "(یه کیبورد ساده و خوش‌دست برای نوشتن)", "alwaysUseInternalKeyboardDescriptionText": "(کیبورد ساده و خوش‌دست برای نوشتن)",
"alwaysUseInternalKeyboardText": "همیشه از کی‌بورد داخلی استفاده شود", "alwaysUseInternalKeyboardText": "همیشه از کی‌بورد داخلی استفاده شود",
"benchmarksText": "معیار و تست استرس", "benchmarksText": "معیار و تست استرس",
"devToolsText": "ابزارهای توسعه", "devToolsText": "ابزارهای توسعه",
@ -1344,8 +1344,8 @@
"translationFetchErrorText": "وضعیت ترجمه در دسترس نیست", "translationFetchErrorText": "وضعیت ترجمه در دسترس نیست",
"translationFetchingStatusText": "چک کردن وضعیت ترجمه ...", "translationFetchingStatusText": "چک کردن وضعیت ترجمه ...",
"translationInformMe": "!وقتی زبان من به‌روزرسانی نیاز داشت، خبرم کن", "translationInformMe": "!وقتی زبان من به‌روزرسانی نیاز داشت، خبرم کن",
"translationNoUpdateNeededText": "!زبان کنونی به‌روز است. ایول به ولت", "translationNoUpdateNeededText": "!زبان بازی به‌روز است. ایول به ولت",
"translationUpdateNeededText": "!زبان کنونی نیاز به به‌روزرسانی دارد", "translationUpdateNeededText": "** !!زبان بازی نیاز به به‌روزرسانی دارد **",
"vrTestingText": "VR تست" "vrTestingText": "VR تست"
}, },
"shareText": "اشتراک‌گذاری", "shareText": "اشتراک‌گذاری",
@ -1399,7 +1399,7 @@
"howToSwitchCharactersText": "(⁦برو \"${SETTINGS} -> ${PLAYER_PROFILES}\" برای ایجاد و شخصی‌سازی شخصیت‌ها به)", "howToSwitchCharactersText": "(⁦برو \"${SETTINGS} -> ${PLAYER_PROFILES}\" برای ایجاد و شخصی‌سازی شخصیت‌ها به)",
"howToUseIconsText": "((در بخش حساب کاربری) برای استفاده از این‌ها نمایه‌های جهانی ایجاد کنید)", "howToUseIconsText": "((در بخش حساب کاربری) برای استفاده از این‌ها نمایه‌های جهانی ایجاد کنید)",
"howToUseMapsText": "(از این نقشه‌ها می‌تونید توی بازی‌های تیمی و تک‌به‌تک خودتون استفاده کنید)", "howToUseMapsText": "(از این نقشه‌ها می‌تونید توی بازی‌های تیمی و تک‌به‌تک خودتون استفاده کنید)",
"iconsText": "نشانهها", "iconsText": "نشانها",
"loadErrorText": "لود شدن صفحه ناموفق بود\nاتصال اینترنت رو چک کنید", "loadErrorText": "لود شدن صفحه ناموفق بود\nاتصال اینترنت رو چک کنید",
"loadingText": "در حال بارگزاری", "loadingText": "در حال بارگزاری",
"mapsText": "نقشه‌ها", "mapsText": "نقشه‌ها",
@ -1423,7 +1423,7 @@
"storeDescriptionText": "8 بازیکن حزب جنون بازی!\n\nدوستان خود (یا رایانه) را در مسابقات مینی بازی‌های انفجاری مانند: پرچم، هاکی و حرکت آهسته\n\nکنترل ساده و پشتیبانی گسترده می‌توان تا حداکثر 8 نفر برای ورود به بازی اقدام کند؛ شما حتی می‌توانید دستگاه‌های تلفن‌همراه خود را به عنوان کنترل از طریق برنامه رایگان BombSquadremote استفاده کنید!\n\nبمباندازی از راه دور!\n\nwww.froemling.net/bombsquad را برای اطلاعات بیشتر چک کنید.", "storeDescriptionText": "8 بازیکن حزب جنون بازی!\n\nدوستان خود (یا رایانه) را در مسابقات مینی بازی‌های انفجاری مانند: پرچم، هاکی و حرکت آهسته\n\nکنترل ساده و پشتیبانی گسترده می‌توان تا حداکثر 8 نفر برای ورود به بازی اقدام کند؛ شما حتی می‌توانید دستگاه‌های تلفن‌همراه خود را به عنوان کنترل از طریق برنامه رایگان BombSquadremote استفاده کنید!\n\nبمباندازی از راه دور!\n\nwww.froemling.net/bombsquad را برای اطلاعات بیشتر چک کنید.",
"storeDescriptions": { "storeDescriptions": {
"blowUpYourFriendsText": "بازیکن دوستات رو بترکون 😁", "blowUpYourFriendsText": "بازیکن دوستات رو بترکون 😁",
"competeInMiniGamesText": "رقابت در بازی‌های کوچک به یژه مسابقه پرواز.", "competeInMiniGamesText": "رقابت در بازی‌های کوچک، از مسابقه تا پرواز.",
"customize2Text": "سفارشی کردن شخصیت‌ها، بازی‌های کوچک، و حتی موسیقی‌های متن .", "customize2Text": "سفارشی کردن شخصیت‌ها، بازی‌های کوچک، و حتی موسیقی‌های متن .",
"customizeText": "شما می تونید شخصیت‌ها رو شخصی‌سازی کنید و به سلیقه خودتون لیست‌بازی درست کنید", "customizeText": "شما می تونید شخصیت‌ها رو شخصی‌سازی کنید و به سلیقه خودتون لیست‌بازی درست کنید",
"sportsMoreFunText": "ورزش ها سرگرم کننده تر میشن با انفجار", "sportsMoreFunText": "ورزش ها سرگرم کننده تر میشن با انفجار",
@ -1443,7 +1443,7 @@
"testBuildValidatedText": "نسخه معتبر است؛ لذت ببرید.!", "testBuildValidatedText": "نسخه معتبر است؛ لذت ببرید.!",
"thankYouText": "تشکر بخاطر حمایت از ما ! از بازی لذت ببرید", "thankYouText": "تشکر بخاطر حمایت از ما ! از بازی لذت ببرید",
"threeKillText": "نابود کردن همزمان سه نفر", "threeKillText": "نابود کردن همزمان سه نفر",
"ticketsDescriptionText": "بلیت‌ها برای بازگشایی شخصیت‌ها، نقشه‌ها، بازی‌های\nکوچک و موارد دیگر در فروشگاه به کار می‌روند. \n\nبلیتها در صندوق‌هایی که از کمپین‌ها، مسابقات\nو دستاوردها برنده شده‌اید، یافت می‌شوند.", "ticketsDescriptionText": "بلیت‌ها برای بازگشایی شخصیت‌ها، نقشه‌ها، بازی‌های\nکوچک و موارد دیگر در فروشگاه به کار می‌روند. \n\nبلیتها در صندوق‌هایی که از کمپین‌ها، ناوردگان\nو دستاوردها برنده شده‌اید، یافت می‌شوند.",
"timeBonusText": "پاداش سرعت عمل", "timeBonusText": "پاداش سرعت عمل",
"timeElapsedText": "زمان گذشته", "timeElapsedText": "زمان گذشته",
"timeExpiredText": "زمان تمام شده", "timeExpiredText": "زمان تمام شده",
@ -1458,7 +1458,7 @@
"getTokensText": "دریافت توکن", "getTokensText": "دریافت توکن",
"notEnoughTokensText": "توکن های شما کافی نیست !", "notEnoughTokensText": "توکن های شما کافی نیست !",
"numTokensText": "⁦توکن ${COUNT}", "numTokensText": "⁦توکن ${COUNT}",
"openNowDescriptionText": "شما به اندازه کافی نشانه دارید\nاکنون این را باز کنید - اگر نکنید\nباید صبر کرد", "openNowDescriptionText": "شما به‌اندازه‌ی کافی توکن برای\nباز کردن آن دارید - نیازی نیست\nصبر کنید.",
"shinyNewCurrencyText": "ارز های جدید بمب اسکواد", "shinyNewCurrencyText": "ارز های جدید بمب اسکواد",
"tokenPack1Text": "بسته توکن کوچک", "tokenPack1Text": "بسته توکن کوچک",
"tokenPack2Text": "بسته توکن متوسط", "tokenPack2Text": "بسته توکن متوسط",
@ -1468,16 +1468,16 @@
"youHaveGoldPassText": ".شما یک گلد پس دارید\n.تمامی خریدهای توکن رایگان است\n!لذت ببرید" "youHaveGoldPassText": ".شما یک گلد پس دارید\n.تمامی خریدهای توکن رایگان است\n!لذت ببرید"
}, },
"topFriendsText": "بالاترین امتیاز دوستان", "topFriendsText": "بالاترین امتیاز دوستان",
"tournamentCheckingStateText": "چک کردن وضعیت مسابقات؛ لطفا صبر کنید", "tournamentCheckingStateText": "چک کردن وضعیت ناوردگان؛ لطفن صبر کنید...",
"tournamentEndedText": "این دوره از مسابقات به پایان رسیده است دوره جدیدی بزودی آغاز خواهد شد", "tournamentEndedText": "این دوره از ناوردگان به پایان رسیده‌است. دوره‌ی جدیدی به‌زودی آغاز خواهد شد.",
"tournamentEntryText": "ورودیِ مسابقات", "tournamentEntryText": "ورودی ناوردگان",
"tournamentFinalStandingsText": "جدول رده بندی نهایی", "tournamentFinalStandingsText": "جدول ردهبندی نهایی",
"tournamentResultsRecentText": "آخرین نتایج مسابقات", "tournamentResultsRecentText": "نتایج ناوردگان اخیر",
"tournamentStandingsText": "جدول رده بندی مسابقات", "tournamentStandingsText": "رده‌بندی ناورد",
"tournamentText": "جام حذفی", "tournamentText": "ناورد",
"tournamentTimeExpiredText": "زمان مسابقات پایان یافت", "tournamentTimeExpiredText": "زمان ناورد پایان یافت",
"tournamentsDisabledWorkspaceText": "وقتی فضاهای کاری فعال هستند، مسابقات غیرفعال می شوند.\n برای فعال کردن مجدد مسابقات، فضای کاری خود را غیرفعال کنید و دوباره راه اندازی کنید.", "tournamentsDisabledWorkspaceText": "وقتی فضاهای کاری فعال هستند، ناوردگان غیرفعال می‌شود.\nبرای فعال کردن دوباره‌ی ناوردگان، فضای کاری خود را غیرفعال کنید و بازراه‌اندازی کنید.",
"tournamentsText": "مسابقات", "tournamentsText": "ناوردگان",
"translations": { "translations": {
"characterNames": { "characterNames": {
"Agent Johnson": "مامور جانسون", "Agent Johnson": "مامور جانسون",
@ -1513,8 +1513,8 @@
"coopLevelNames": { "coopLevelNames": {
"${GAME} Training": "تمرین ${GAME}", "${GAME} Training": "تمرین ${GAME}",
"Infinite ${GAME}": "${GAME} بی‌پایان", "Infinite ${GAME}": "${GAME} بی‌پایان",
"Infinite Onslaught": "نبرد بی‌پایان", "Infinite Onslaught": "نبرد بی‌پایان",
"Infinite Runaround": "دور بی‌پایان", "Infinite Runaround": "دور بی‌پایان",
"Onslaught Training": "نبرد مبتدی", "Onslaught Training": "نبرد مبتدی",
"Pro ${GAME}": "${GAME} حرفه‌ای", "Pro ${GAME}": "${GAME} حرفه‌ای",
"Pro Football": "فوتبال حرفه‌ای", "Pro Football": "فوتبال حرفه‌ای",
@ -1604,17 +1604,17 @@
"Death Match": "نبرد مرگبار", "Death Match": "نبرد مرگبار",
"Easter Egg Hunt": "شکار ایستر اگ", "Easter Egg Hunt": "شکار ایستر اگ",
"Elimination": "استقامت", "Elimination": "استقامت",
"Football": "فوتبال آمریکایی", "Football": "فوتبال آمریکایی",
"Hockey": "هاکی", "Hockey": "هاکی",
"Keep Away": "دور نگه داشتن", "Keep Away": "دور نگه داشتن",
"King of the Hill": "پادشاه تپه", "King of the Hill": "پادشاه تپه",
"Meteor Shower": "بمب‌باران", "Meteor Shower": "بمب‌باران",
"Ninja Fight": "نبرد با نینجاها", "Ninja Fight": "نبرد با نینجاها",
"Onslaught": "مبارزه", "Onslaught": "مبارزه",
"Race": "مسابقه‌ی دو", "Race": "مسابقه‌ی دو",
"Runaround": "مانع", "Runaround": "مانع",
"Target Practice": "تمرین بمب‌اندازی", "Target Practice": "تمرین بمب‌اندازی",
"The Last Stand": "دفاع آخر" "The Last Stand": "دفاع آخر"
}, },
"inputDeviceNames": { "inputDeviceNames": {
"Keyboard": "کی‌بورد", "Keyboard": "کی‌بورد",
@ -1643,6 +1643,7 @@
"Indonesian": "اندونزیایی", "Indonesian": "اندونزیایی",
"Italian": "ایتالیایی", "Italian": "ایتالیایی",
"Japanese": "ژاپنی", "Japanese": "ژاپنی",
"Kazakh": "قزاقی",
"Korean": "کره‌ای", "Korean": "کره‌ای",
"Malay": "مالایی", "Malay": "مالایی",
"Persian": "⁦فارسی‎", "Persian": "⁦فارسی‎",
@ -1723,13 +1724,13 @@
"Could not establish a secure connection.": "نمیتوان یک اتصال امن ایجاد کرد", "Could not establish a secure connection.": "نمیتوان یک اتصال امن ایجاد کرد",
"Daily maximum reached.": "به حداکثر ارقام روزانه رسیدید", "Daily maximum reached.": "به حداکثر ارقام روزانه رسیدید",
"Daily sign-in reward": "پاداش ورود روزانه", "Daily sign-in reward": "پاداش ورود روزانه",
"Entering tournament...": "ورود به مسابقات...", "Entering tournament...": "ورود به ناورد...",
"Higher streaks lead to better rewards.": "رگه های بالاتر منجر به پاداش بهتر می شود.", "Higher streaks lead to better rewards.": "رگه های بالاتر منجر به پاداش بهتر می شود.",
"Invalid code.": "کد نامعتبر", "Invalid code.": "کد نامعتبر",
"Invalid payment; purchase canceled.": "پرداخت نامعتبر؛ خرید لغو شد.", "Invalid payment; purchase canceled.": "پرداخت نامعتبر؛ خرید لغو شد.",
"Invalid promo code.": "کد دعوتی نامعتبر است.", "Invalid promo code.": "کد دعوتی نامعتبر است.",
"Invalid purchase.": "خرید نا معتبر", "Invalid purchase.": "خرید نا معتبر",
"Invalid tournament entry; score will be ignored.": "ورود مسابقات نامعتبر است؛ نمره نادیده گرفته می شود.", "Invalid tournament entry; score will be ignored.": "ورود نامعتبر به ناورد؛ امتیاز لحاظ نمی‌شود.",
"Item unlocked!": "!مورد باز شد", "Item unlocked!": "!مورد باز شد",
"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)": "پیوند کردن ممنوع شد ${ACCOUNT} شامل\nداده های قابل توجهی که می توانند از دست بدهند.\nشما می توانید در صورت مخالفت پیوست کنید اگر دوست دارید\n(و در عوض داده های این حساب را از دست می دهید)", "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)": "پیوند کردن ممنوع شد ${ACCOUNT} شامل\nداده های قابل توجهی که می توانند از دست بدهند.\nشما می توانید در صورت مخالفت پیوست کنید اگر دوست دارید\n(و در عوض داده های این حساب را از دست می دهید)",
"Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "را به این حساب پیوند دهید؟ ${ACCOUNT} آیا مایلید که حساب کاربری \n.از بین خواهد رفت ${ACCOUNT} تمام اطلاعات روی حساب\nاین کار برگشت‌پذیر نیست. آیا مطمئنید؟", "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "را به این حساب پیوند دهید؟ ${ACCOUNT} آیا مایلید که حساب کاربری \n.از بین خواهد رفت ${ACCOUNT} تمام اطلاعات روی حساب\nاین کار برگشت‌پذیر نیست. آیا مطمئنید؟",
@ -1738,7 +1739,7 @@
"Max number of profiles reached.": ".تعداد نمایه‌ها به حداکثر رسیده است", "Max number of profiles reached.": ".تعداد نمایه‌ها به حداکثر رسیده است",
"Maximum friend code rewards reached.": ".حداکثر جایزه کد ارسالی برای دوستان دریافت شد", "Maximum friend code rewards reached.": ".حداکثر جایزه کد ارسالی برای دوستان دریافت شد",
"Message is too long.": "پیام خیلی طولانی است", "Message is too long.": "پیام خیلی طولانی است",
"New tournament result!": "نتیجه مسابقات جدید!", "New tournament result!": "نتیجه‌ی ناورد جدید!",
"No servers are available. Please try again soon.": "هیچ سروری در دسترس نیست. لطفا به زودی دوباره امتحان کنید", "No servers are available. Please try again soon.": "هیچ سروری در دسترس نیست. لطفا به زودی دوباره امتحان کنید",
"No slots available. Free a slot and try again.": "محل صندوقی موجود نیست. یک محل صندوق ی آزاد کنید و دوباره امتحان کنید.", "No slots available. Free a slot and try again.": "محل صندوقی موجود نیست. یک محل صندوق ی آزاد کنید و دوباره امتحان کنید.",
"Profile \"${NAME}\" upgraded successfully.": ".با موفقیت ارتقا یافت «${NAME}» نمایهٔ", "Profile \"${NAME}\" upgraded successfully.": ".با موفقیت ارتقا یافت «${NAME}» نمایهٔ",
@ -1754,12 +1755,12 @@
"Still searching for nearby servers; please try again soon.": "هنوز سرورهای اطراف را جستجو می کنید. لطفا به زودی دوباره امتحان کنید", "Still searching for nearby servers; please try again soon.": "هنوز سرورهای اطراف را جستجو می کنید. لطفا به زودی دوباره امتحان کنید",
"Streak: ${NUM} days": "‎روز ${NUM} دوره‌ی ورود روزانه:", "Streak: ${NUM} days": "‎روز ${NUM} دوره‌ی ورود روزانه:",
"Temporarily unavailable; please try again later.": "در حال حاضر این گذینه موجود نمی باشد؛لطفا بعدا امتحان کنید", "Temporarily unavailable; please try again later.": "در حال حاضر این گذینه موجود نمی باشد؛لطفا بعدا امتحان کنید",
"The tournament ended before you finished.": "مسابقات به پایان رسید قبل از اینکه شما به پایان برسید.", "The tournament ended before you finished.": "ناورد به پایان رسید، پیش از اینکه شما به پایان برسید.",
"This account cannot be unlinked for ${NUM} days.": "این حساب برای مدت ${NUM} روز قابل جداسازی نیست!", "This account cannot be unlinked for ${NUM} days.": "این حساب برای مدت ${NUM} روز قابل جداسازی نیست!",
"This code cannot be used on the account that created it.": "کد در حسابی که با آن ساخته شده قابل استفاده نیست", "This code cannot be used on the account that created it.": "کد در حسابی که با آن ساخته شده قابل استفاده نیست",
"This is currently unavailable; please try again later.": ".این گزینه در حال حاضر در دسترس نیست; لطفا بعدا دوباره تلاش کنید", "This is currently unavailable; please try again later.": ".این گزینه در حال حاضر در دسترس نیست; لطفا بعدا دوباره تلاش کنید",
"This requires version ${VERSION} or newer.": "یا جدیدتر باشد ${VERSION} برای این کار نسخه ی بازی باید", "This requires version ${VERSION} or newer.": "یا جدیدتر باشد ${VERSION} برای این کار نسخه ی بازی باید",
"Tournaments disabled due to rooted device.": "مسابقات به دلیل روت بودن دستگاه غیر فعال شده است", "Tournaments disabled due to rooted device.": "ناوردگان به‌دلیل روت بودن دستگاه غیرفعال شده‌است.",
"Tournaments require ${VERSION} or newer": "تورنومنت ها به${VERSION}جدید تر یا حرفه ای نیاز دارند", "Tournaments require ${VERSION} or newer": "تورنومنت ها به${VERSION}جدید تر یا حرفه ای نیاز دارند",
"Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "آیا مایلبد حساب ${ACCOUNT} را از حساب خودتان جداسازی کنید؟\nتمام اطلاعات حساب ${ACCOUNT} بازنویسی خواهد شد.\n(به جز بعضی از افتخارات کسب شده)", "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "آیا مایلبد حساب ${ACCOUNT} را از حساب خودتان جداسازی کنید؟\nتمام اطلاعات حساب ${ACCOUNT} بازنویسی خواهد شد.\n(به جز بعضی از افتخارات کسب شده)",
"WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "اخطار: شکایت هایی مبنی بر استفاده شما از ابزار های تقلب به دست ما رسیده!\nدر صورت تکرار حساب کاربری شما مسدود خواهد شد! لطفا جوانمردانه بازی کنید.", "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "اخطار: شکایت هایی مبنی بر استفاده شما از ابزار های تقلب به دست ما رسیده!\nدر صورت تکرار حساب کاربری شما مسدود خواهد شد! لطفا جوانمردانه بازی کنید.",
@ -1777,12 +1778,13 @@
"You got an achievement reward!": "!یه پاداش دستاورد گرفتی", "You got an achievement reward!": "!یه پاداش دستاورد گرفتی",
"You have been promoted to a new league; congratulations!": "شما به یک لیگ جدید ارتقا داده‌اید؛ تبریک می‌گوییم!", "You have been promoted to a new league; congratulations!": "شما به یک لیگ جدید ارتقا داده‌اید؛ تبریک می‌گوییم!",
"You lost a chest! (All your chest slots were full)": "یک صندوق از دست دادی! (همه محل های صندوق شما پر بود)", "You lost a chest! (All your chest slots were full)": "یک صندوق از دست دادی! (همه محل های صندوق شما پر بود)",
"You must sign in to do this.": "برای انجام اینکار باید وارد سیستم شوید.",
"You must update the app to view this.": "برای دیدن این بخش باید بازی را بروزرسانی کنید", "You must update the app to view this.": "برای دیدن این بخش باید بازی را بروزرسانی کنید",
"You must update to a newer version of the app to do this.": "برای انجام این کار شما باید بازی را به روز رسانی کنید", "You must update to a newer version of the app to do this.": "برای انجام این کار شما باید بازی را به روز رسانی کنید",
"You must update to the newest version of the game to do this.": "برای انجام این کار باید از آخرین نسخه بازی استفاده کنید.", "You must update to the newest version of the game to do this.": "برای انجام این کار باید از آخرین نسخه بازی استفاده کنید.",
"You must wait a few seconds before entering a new code.": "شما باید چند ثانیه قبل از وارد کردن یک کد جدید صبر کنید.", "You must wait a few seconds before entering a new code.": "شما باید چند ثانیه قبل از وارد کردن یک کد جدید صبر کنید.",
"You placed #${RANK} in a tournament!": "شما #${RANK} را در یک تورنمنت قرار دادید!", "You placed #${RANK} in a tournament!": "شما #${RANK} را در یک تورنمنت قرار دادید!",
"You ranked #${RANK} in the last tournament. Thanks for playing!": "! شدید . ممنون که شرکت کردید ${RANK} شما در آخرین مسابقه رتبه ی", "You ranked #${RANK} in the last tournament. Thanks for playing!": "!⁦شدید. ممنون که شرکت کردید #${RANK} شما در آخرین ناورد رتبه‌ی",
"Your account was rejected. Are you signed in?": "حساب شما رد شده است. آیا وارد حساب خود شده اید؟", "Your account was rejected. Are you signed in?": "حساب شما رد شده است. آیا وارد حساب خود شده اید؟",
"Your ad views are not registering. Ad options will be limited for a while.": "بازدیدهای تبلیغاتی شما ثبت نمی شود. گزینه های تبلیغاتی برای مدتی محدود خواهد بود.", "Your ad views are not registering. Ad options will be limited for a while.": "بازدیدهای تبلیغاتی شما ثبت نمی شود. گزینه های تبلیغاتی برای مدتی محدود خواهد بود.",
"Your copy of the game has been modified.\nPlease revert any changes and try again.": "نسخه بازی شما دستکاری شده است.\nلطفا تغییرات رو به حالت اول برگردونید و دوباره امتحان کنید.", "Your copy of the game has been modified.\nPlease revert any changes and try again.": "نسخه بازی شما دستکاری شده است.\nلطفا تغییرات رو به حالت اول برگردونید و دوباره امتحان کنید.",
@ -1907,7 +1909,7 @@
"phrase12Text": "اگه می‌خوای یه مشت خفن بزنی که فکش بیاد پایین، باید بدویی و بچرخی.", "phrase12Text": "اگه می‌خوای یه مشت خفن بزنی که فکش بیاد پایین، باید بدویی و بچرخی.",
"phrase13Text": "اوخ! بابت اینکه فکت اومد پایین ببخشید ${NAME} جان.", "phrase13Text": "اوخ! بابت اینکه فکت اومد پایین ببخشید ${NAME} جان.",
"phrase14Text": "خیلی چیزها رو می‌شه برداشت و پرتاب کرد. مثل پرچم... یا ${NAME}", "phrase14Text": "خیلی چیزها رو می‌شه برداشت و پرتاب کرد. مثل پرچم... یا ${NAME}",
"phrase15Text": "نوبتیم باشه، نوبت بمب‌هاست.", "phrase15Text": "نوبتی هم باشه، نوبت بمب‌هاست.",
"phrase16Text": "بمب انداختن یه کم تمرین لازم داره.", "phrase16Text": "بمب انداختن یه کم تمرین لازم داره.",
"phrase17Text": "اوخ اوخ! اصلا پرتاب خوبی نبود.", "phrase17Text": "اوخ اوخ! اصلا پرتاب خوبی نبود.",
"phrase18Text": "حرکت کردن باعث می‌شه تا بتونی دورتر پرتاب کنی.", "phrase18Text": "حرکت کردن باعث می‌شه تا بتونی دورتر پرتاب کنی.",
@ -1935,9 +1937,9 @@
"twoKillText": "دو نفرو با هم کشتی!", "twoKillText": "دو نفرو با هم کشتی!",
"uiScaleText": "UI مقیاس", "uiScaleText": "UI مقیاس",
"unavailableText": "در دسترس نیست", "unavailableText": "در دسترس نیست",
"unclaimedPrizesText": "شما جایزه های بی ادعایی دارید", "unclaimedPrizesText": "جایزه‌های نگرفته‌ای دارید!",
"unconfiguredControllerDetectedText": ":کنترلر پیکربندی نشده شناسایی شد", "unconfiguredControllerDetectedText": ":کنترلر پیکربندینشده شناسایی شد",
"unlockThisInTheStoreText": ". این مورد باید در فروشگاه باز شود", "unlockThisInTheStoreText": "این مورد باید در فروشگاه باز شود.",
"unlockThisProfilesText": "برای ایجاد بیش از ${NUM} پروفایل٫ احتیاج به این موارد دارید:", "unlockThisProfilesText": "برای ایجاد بیش از ${NUM} پروفایل٫ احتیاج به این موارد دارید:",
"unlockThisText": ": برا باز کردن قفل این شما نیاز دارید که", "unlockThisText": ": برا باز کردن قفل این شما نیاز دارید که",
"unsupportedControllerText": "متاسفانه کنترلر \"${NAME}\" پشتیبانی نمی‌شود.", "unsupportedControllerText": "متاسفانه کنترلر \"${NAME}\" پشتیبانی نمی‌شود.",
@ -2007,8 +2009,8 @@
"winsPlayerText": "!⁦برنده شد ${NAME}", "winsPlayerText": "!⁦برنده شد ${NAME}",
"winsTeamText": "!⁦برنده شد ${NAME}", "winsTeamText": "!⁦برنده شد ${NAME}",
"winsText": "!⁦برنده شد ${NAME}", "winsText": "!⁦برنده شد ${NAME}",
"workspaceSyncErrorText": "خطا در همگام‌سازی ${WORKSPACE}. برای جزئیات به لاگ مراجعه کنید.", "workspaceSyncErrorText": ".⁦برای جزییات لاگ را ببینید .${WORKSPACE} خطا در همگام‌سازی",
"workspaceSyncReuseText": "نمی‌توان ${WORKSPACE} را همگام‌سازی کرد. استفادهٔ مجدد از نسخهٔ همگام‌سازی‌شده قبلی.", "workspaceSyncReuseText": ".را همگام‌سازی کرد. استفادهٔ مجدد از نسخهٔ همگام‌سازی‌شده قبلی ${WORKSPACE} نمی‌توان",
"worldScoresUnavailableText": "امتیاز های جهانی قابل دسترس نیستند.", "worldScoresUnavailableText": "امتیاز های جهانی قابل دسترس نیستند.",
"worldsBestScoresText": "بهترین امتیازهای جهانی", "worldsBestScoresText": "بهترین امتیازهای جهانی",
"worldsBestTimesText": "بهترین زمان های جهانی", "worldsBestTimesText": "بهترین زمان های جهانی",

View file

@ -1620,7 +1620,8 @@
"Arabic": "Arrr, Arabic be it!", "Arabic": "Arrr, Arabic be it!",
"Belarussian": "Belarusian Scallywag", "Belarussian": "Belarusian Scallywag",
"Chinese": "Chinesey Simplified", "Chinese": "Chinesey Simplified",
"ChineseTraditional": "Chinaman's Olde", "ChineseSimplified": "Chinaman's - Simply",
"ChineseTraditional": "Chinaman's - Olde",
"Croatian": "Croatian lass", "Croatian": "Croatian lass",
"Czech": "Czech, matey!", "Czech": "Czech, matey!",
"Danish": "Danish scallywag", "Danish": "Danish scallywag",
@ -1638,17 +1639,22 @@
"Indonesian": "Indonesian be", "Indonesian": "Indonesian be",
"Italian": "Italin'", "Italian": "Italin'",
"Japanese": "Japanese", "Japanese": "Japanese",
"Kazakh": "Kazakh, arr!!",
"Korean": "Korey", "Korean": "Korey",
"Malay": "Malaysie", "Malay": "Malaysie",
"Persian": "Persian Sea Dog", "Persian": "Persian Sea Dog",
"PirateSpeak": "Pirate Speak", "PirateSpeak": "Pirate Speak",
"Polish": "Polish", "Polish": "Polish",
"Portuguese": "Portuguese, matey!", "Portuguese": "Portuguese, matey!",
"PortugueseBrazil": "Planktuguese - Brazily",
"PortuguesePortugal": "Portugalian Portugalese",
"Romanian": "Romanian, me hearty!", "Romanian": "Romanian, me hearty!",
"Russian": "Russian", "Russian": "Russian",
"Serbian": "Serbiarr", "Serbian": "Serbiarr",
"Slovak": "Slovak scallywag", "Slovak": "Slovak scallywag",
"Spanish": "Spanish matey", "Spanish": "Spanish matey",
"SpanishLatinAmerica": "Spanish!! - Latin America",
"SpanishSpain": "Spanish!! - Spooin",
"Swedish": "Swedisher", "Swedish": "Swedisher",
"Tamil": "Tamin", "Tamil": "Tamin",
"Thai": "Thai be", "Thai": "Thai be",
@ -1767,6 +1773,7 @@
"You got an achievement reward!": "Yer got a achievement shiny!", "You got an achievement reward!": "Yer got a achievement shiny!",
"You have been promoted to a new league; congratulations!": "Ye be promoted to a new league; congrats, matey!", "You have been promoted to a new league; congratulations!": "Ye be promoted to a new league; congrats, matey!",
"You lost a chest! (All your chest slots were full)": "Arr! Ye be lost yer treasure! (All room for booty were taken)", "You lost a chest! (All your chest slots were full)": "Arr! Ye be lost yer treasure! (All room for booty were taken)",
"You must sign in to do this.": "Ye m'st set sail firrst t' d' t's, matey!!",
"You must update the app to view this.": "Ye be needin' t' update th' app t' see this.", "You must update the app to view this.": "Ye be needin' t' update th' app t' see this.",
"You must update to a newer version of the app to do this.": "Ye be needin' t' upgrade t' a fresh version o' th' app t' do this.", "You must update to a newer version of the app to do this.": "Ye be needin' t' upgrade t' a fresh version o' th' app t' do this.",
"You must update to the newest version of the game to do this.": "Ye be needin' t' update t' the latest version o' th' game t' do this.", "You must update to the newest version of the game to do this.": "Ye be needin' t' update t' the latest version o' th' game t' do this.",

View file

@ -1094,7 +1094,7 @@
"macControllerSubsystemMFiText": "Zrobione-dla-iOS/Mac", "macControllerSubsystemMFiText": "Zrobione-dla-iOS/Mac",
"macControllerSubsystemTitleText": "Wsparcie Kontrolerów", "macControllerSubsystemTitleText": "Wsparcie Kontrolerów",
"mainMenu": { "mainMenu": {
"creditsText": "Info", "creditsText": "Twórcy",
"demoMenuText": "Menu Demo", "demoMenuText": "Menu Demo",
"endGameText": "Koniec Gry", "endGameText": "Koniec Gry",
"endTestText": "Zakończ test", "endTestText": "Zakończ test",
@ -1122,7 +1122,7 @@
"modeArcadeText": "Tryb Salonu Gier", "modeArcadeText": "Tryb Salonu Gier",
"modeClassicText": "Tryb Klasyczny", "modeClassicText": "Tryb Klasyczny",
"modeDemoText": "Tryb Demo", "modeDemoText": "Tryb Demo",
"moreSoonText": "Przybędzie w przyszłości...", "moreSoonText": "Więcej wkrótce...",
"mostDestroyedPlayerText": "Najbardziej Zgładzony Gracz", "mostDestroyedPlayerText": "Najbardziej Zgładzony Gracz",
"mostValuablePlayerText": "Najwartościowszy gracz", "mostValuablePlayerText": "Najwartościowszy gracz",
"mostViolatedPlayerText": "Gracz najbardziej sprofanowany", "mostViolatedPlayerText": "Gracz najbardziej sprofanowany",
@ -1337,7 +1337,7 @@
"retryText": "Ponów", "retryText": "Ponów",
"revertText": "Przywróć", "revertText": "Przywróć",
"runBoldText": "URUCHOM", "runBoldText": "URUCHOM",
"runText": "Uruchom", "runText": "Bieg",
"saveText": "Zapisz", "saveText": "Zapisz",
"scanScriptsErrorText": "Błąd w skanowaniu skryptów. Sprawdź konsolę dla szczegółów.", "scanScriptsErrorText": "Błąd w skanowaniu skryptów. Sprawdź konsolę dla szczegółów.",
"scanScriptsMultipleModulesNeedUpdatesText": "${PATH} oraz ${NUM} innych modułów potrzebują aktualizacji do api ${API}.", "scanScriptsMultipleModulesNeedUpdatesText": "${PATH} oraz ${NUM} innych modułów potrzebują aktualizacji do api ${API}.",
@ -1703,7 +1703,8 @@
"Arabic": "Arabski", "Arabic": "Arabski",
"Belarussian": "Białoruski", "Belarussian": "Białoruski",
"Chinese": "Chiński Uproszczony", "Chinese": "Chiński Uproszczony",
"ChineseTraditional": "Chiński Tradycyjny", "ChineseSimplified": "Chiński - Prosty",
"ChineseTraditional": "Chiński - Tradycyjny",
"Croatian": "Chorwacki", "Croatian": "Chorwacki",
"Czech": "Czeski", "Czech": "Czeski",
"Danish": "Duński", "Danish": "Duński",
@ -1727,11 +1728,15 @@
"PirateSpeak": "Piracki język", "PirateSpeak": "Piracki język",
"Polish": "Polski", "Polish": "Polski",
"Portuguese": "Portugalski", "Portuguese": "Portugalski",
"PortugueseBrazil": "Portugalski (Brazylia)",
"PortuguesePortugal": "Portugalski (Portugalia)",
"Romanian": "Rumuński", "Romanian": "Rumuński",
"Russian": "Rosyjski", "Russian": "Rosyjski",
"Serbian": "Serbski", "Serbian": "Serbski",
"Slovak": "słowacki", "Slovak": "słowacki",
"Spanish": "Hiszpański", "Spanish": "Hiszpański",
"SpanishLatinAmerica": "Hiszpański (Ameryka Łacińska)",
"SpanishSpain": "Hiszpański (Hiszpania)",
"Swedish": "Szwedzki", "Swedish": "Szwedzki",
"Tamil": "Tamil", "Tamil": "Tamil",
"Thai": "Tajski", "Thai": "Tajski",

View file

@ -1065,7 +1065,7 @@
"modeClassicText": "Modo Clássico", "modeClassicText": "Modo Clássico",
"modeDemoText": "Modo Demonstração", "modeDemoText": "Modo Demonstração",
"moreSoonText": "Mais novidades em breve...", "moreSoonText": "Mais novidades em breve...",
"mostDestroyedPlayerText": "Player Mais Destruído", "mostDestroyedPlayerText": "Jogador Mais Destruído",
"mostValuablePlayerText": "Jogador Mais Valioso", "mostValuablePlayerText": "Jogador Mais Valioso",
"mostViolatedPlayerText": "Jogador Mais Violado", "mostViolatedPlayerText": "Jogador Mais Violado",
"mostViolentPlayerText": "Jogador Mais Violento", "mostViolentPlayerText": "Jogador Mais Violento",
@ -1075,7 +1075,7 @@
"mustInviteFriendsText": "Nota: você deve convidar amigos no\npainel \"${GATHER}\" ou adicionar\ncontroles para jogar no multijogador.", "mustInviteFriendsText": "Nota: você deve convidar amigos no\npainel \"${GATHER}\" ou adicionar\ncontroles para jogar no multijogador.",
"nameBetrayedText": "${NAME} traiu ${VICTIM}.", "nameBetrayedText": "${NAME} traiu ${VICTIM}.",
"nameDiedText": "${NAME} morreu.", "nameDiedText": "${NAME} morreu.",
"nameKilledText": "${NAME} espancou ${VICTIM}.", "nameKilledText": "${NAME} matou ${VICTIM}.",
"nameNotEmptyText": "Nome não pode estar vazio!", "nameNotEmptyText": "Nome não pode estar vazio!",
"nameScoresText": "${NAME} fez um ponto!", "nameScoresText": "${NAME} fez um ponto!",
"nameSuicideKidFriendlyText": "${NAME} acidentalmente morreu.", "nameSuicideKidFriendlyText": "${NAME} acidentalmente morreu.",
@ -1621,6 +1621,7 @@
"Indonesian": "Indonésio", "Indonesian": "Indonésio",
"Italian": "Italiano", "Italian": "Italiano",
"Japanese": "Japonês", "Japanese": "Japonês",
"Kazakh": "cazaque",
"Korean": "Coreano", "Korean": "Coreano",
"Malay": "Malaio", "Malay": "Malaio",
"Persian": "Persa", "Persian": "Persa",
@ -1754,6 +1755,7 @@
"You got an achievement reward!": "Você ganhou uma recompensa por conquista!", "You got an achievement reward!": "Você ganhou uma recompensa por conquista!",
"You have been promoted to a new league; congratulations!": "Você foi promovido a uma nova liga; parabéns!", "You have been promoted to a new league; congratulations!": "Você foi promovido a uma nova liga; parabéns!",
"You lost a chest! (All your chest slots were full)": "Você perdeu um baú! (Todos os seus espaços de baú estavam cheios)", "You lost a chest! (All your chest slots were full)": "Você perdeu um baú! (Todos os seus espaços de baú estavam cheios)",
"You must sign in to do this.": "Você deve iniciar sessão para fazer isto.",
"You must update the app to view this.": "Você deve atualizar o aplicativo para visualizar isso.", "You must update the app to view this.": "Você deve atualizar o aplicativo para visualizar isso.",
"You must update to a newer version of the app to do this.": "Você deve atualizar para uma nova versão do aplicativo para fazer isso.", "You must update to a newer version of the app to do this.": "Você deve atualizar para uma nova versão do aplicativo para fazer isso.",
"You must update to the newest version of the game to do this.": "Você deve atualizar para a nova versão do jogo para fazer isso.", "You must update to the newest version of the game to do this.": "Você deve atualizar para a nova versão do jogo para fazer isso.",

View file

@ -1065,7 +1065,7 @@
"modeClassicText": "Modo Clássico", "modeClassicText": "Modo Clássico",
"modeDemoText": "Modo Demonstração", "modeDemoText": "Modo Demonstração",
"moreSoonText": "Mais novidades em breve...", "moreSoonText": "Mais novidades em breve...",
"mostDestroyedPlayerText": "Player Mais Destruído", "mostDestroyedPlayerText": "Jogador Mais Destruído",
"mostValuablePlayerText": "Jogador Mais Valioso", "mostValuablePlayerText": "Jogador Mais Valioso",
"mostViolatedPlayerText": "Jogador Mais Violado", "mostViolatedPlayerText": "Jogador Mais Violado",
"mostViolentPlayerText": "Jogador Mais Violento", "mostViolentPlayerText": "Jogador Mais Violento",
@ -1075,7 +1075,7 @@
"mustInviteFriendsText": "Nota: você deve convidar amigos no\npainel \"${GATHER}\" ou adicionar\ncontroles para jogar no multijogador.", "mustInviteFriendsText": "Nota: você deve convidar amigos no\npainel \"${GATHER}\" ou adicionar\ncontroles para jogar no multijogador.",
"nameBetrayedText": "${NAME} traiu ${VICTIM}.", "nameBetrayedText": "${NAME} traiu ${VICTIM}.",
"nameDiedText": "${NAME} morreu.", "nameDiedText": "${NAME} morreu.",
"nameKilledText": "${NAME} espancou ${VICTIM}.", "nameKilledText": "${NAME} matou ${VICTIM}.",
"nameNotEmptyText": "Nome não pode estar vazio!", "nameNotEmptyText": "Nome não pode estar vazio!",
"nameScoresText": "${NAME} fez um ponto!", "nameScoresText": "${NAME} fez um ponto!",
"nameSuicideKidFriendlyText": "${NAME} acidentalmente morreu.", "nameSuicideKidFriendlyText": "${NAME} acidentalmente morreu.",

View file

@ -7,6 +7,7 @@
"campaignProgressText": "Progres Campanie [Greu]: ${PROGRESS}", "campaignProgressText": "Progres Campanie [Greu]: ${PROGRESS}",
"changeOncePerSeason": "Acest lucru poate fi schimbat o singură dată pe sezon.", "changeOncePerSeason": "Acest lucru poate fi schimbat o singură dată pe sezon.",
"changeOncePerSeasonError": "Trebuie să aștepți până la următorul sezon (timp de ${NUM} (de) zile) dacă vrei să schimbi acest lucru din nou", "changeOncePerSeasonError": "Trebuie să aștepți până la următorul sezon (timp de ${NUM} (de) zile) dacă vrei să schimbi acest lucru din nou",
"createAnAccountText": "Creează un cont",
"customName": "Nume Personalizat", "customName": "Nume Personalizat",
"deleteAccountText": "Șterge contul", "deleteAccountText": "Șterge contul",
"deviceSpecificAccountText": "Foloseşti un cont specific dispozitivului: ${NAME}", "deviceSpecificAccountText": "Foloseşti un cont specific dispozitivului: ${NAME}",
@ -378,6 +379,14 @@
"chatMuteText": "Amuțește Chat-ul", "chatMuteText": "Amuțește Chat-ul",
"chatMutedText": "Chat-ul Este Amuțit", "chatMutedText": "Chat-ul Este Amuțit",
"chatUnMuteText": "Dezamuțește Chat-ul", "chatUnMuteText": "Dezamuțește Chat-ul",
"chests": {
"prizeOddsText": "Șanse la Premii",
"reduceWaitText": "Reducere a timpului de așteptare",
"slotDescriptionText": "Acest slot poate ține o ladă.\n\nCâștigă lăzi jucând niveluri din campanie,\nclasându-te în turnee și completând \nrealizări.",
"slotText": "Slot ladă ${NUM}",
"slotsFullWarningText": "ATENȚIE: Toate sloturile tale pentru lăzi sunt pline.\nOrice ladă câștigată în acest joc va fi pierdută.",
"unlocksInText": "Se deblochează în"
},
"choosingPlayerText": "<se alege jucătorul>", "choosingPlayerText": "<se alege jucătorul>",
"claimText": "Revendică", "claimText": "Revendică",
"codesExplainText": "Codurile sunt furnizate de dezvoltator pentru\ndiagnosticați și corectați problemele legate de cont.", "codesExplainText": "Codurile sunt furnizate de dezvoltator pentru\ndiagnosticați și corectați problemele legate de cont.",
@ -660,6 +669,8 @@
"errorText": "Eroare", "errorText": "Eroare",
"errorUnknownText": "eroare necunoscută", "errorUnknownText": "eroare necunoscută",
"exitGameText": "Ieși din ${APP_NAME}?", "exitGameText": "Ieși din ${APP_NAME}?",
"expiredAgoText": "Expirat acum ${T}",
"expiresInText": "Expiră în ${T}",
"exportSuccessText": "'${NAME}' a fost exportat cu succes.", "exportSuccessText": "'${NAME}' a fost exportat cu succes.",
"externalStorageText": "Memorie Externă", "externalStorageText": "Memorie Externă",
"failText": "Eșec", "failText": "Eșec",
@ -722,6 +733,7 @@
"copyCodeConfirmText": "Codul a fost copiat în clipboard.", "copyCodeConfirmText": "Codul a fost copiat în clipboard.",
"copyCodeText": "Copiază Codul", "copyCodeText": "Copiază Codul",
"dedicatedServerInfoText": "Pentru cele mai bune rezultate, configurează un server dedicat. Consultă bombsquadgame.com/server pentru a afla cum.", "dedicatedServerInfoText": "Pentru cele mai bune rezultate, configurează un server dedicat. Consultă bombsquadgame.com/server pentru a afla cum.",
"descriptionShortText": "Folosește fereastra de adunare pentru a forma o echipă.",
"disconnectClientsText": "Această opțiune va deconecta ${COUNT} jucător(i) din\njocul tău curent. Ești sigur?", "disconnectClientsText": "Această opțiune va deconecta ${COUNT} jucător(i) din\njocul tău curent. Ești sigur?",
"earnTicketsForRecommendingAmountText": "Prietenii tăi vor primi ${COUNT} de bilete dacă vor încerca jocul \n(iar tu vei primi ${YOU_COUNT} pentru fiecare care încearcă)", "earnTicketsForRecommendingAmountText": "Prietenii tăi vor primi ${COUNT} de bilete dacă vor încerca jocul \n(iar tu vei primi ${YOU_COUNT} pentru fiecare care încearcă)",
"earnTicketsForRecommendingText": "Împărtăşeşte jocul\npentru bilete gratuite...", "earnTicketsForRecommendingText": "Împărtăşeşte jocul\npentru bilete gratuite...",
@ -970,12 +982,14 @@
"timeOutText": "(i se va pierde controlul meniului în ${TIME} (de) secunde)", "timeOutText": "(i se va pierde controlul meniului în ${TIME} (de) secunde)",
"touchScreenJoinWarningText": "Ai intrat cu touchscreen-ul.\nDacă ai făcut-o din greşeală, apasă 'Meniu -> Devino Spectator' cu acesta.", "touchScreenJoinWarningText": "Ai intrat cu touchscreen-ul.\nDacă ai făcut-o din greşeală, apasă 'Meniu -> Devino Spectator' cu acesta.",
"touchScreenText": "TouchScreen", "touchScreenText": "TouchScreen",
"unableToCompleteTryAgainText": "Nu se poate finaliza acum.\nTe rugăm să încerci din nou.",
"unableToResolveHostText": "Eroare: imposibil de găsit hostul.", "unableToResolveHostText": "Eroare: imposibil de găsit hostul.",
"unavailableNoConnectionText": "Acest serviciu nu este disponibil acum (fără conexiune la internet?)", "unavailableNoConnectionText": "Acest serviciu nu este disponibil acum (fără conexiune la internet?)",
"vrOrientationResetCardboardText": "Foloseşte această opțiune pentru resetare orientării VR.\nDacă vrei să te joci vei avea nevoie de un controller external.", "vrOrientationResetCardboardText": "Foloseşte această opțiune pentru resetare orientării VR.\nDacă vrei să te joci vei avea nevoie de un controller external.",
"vrOrientationResetText": "Orientare VR resetată.", "vrOrientationResetText": "Orientare VR resetată.",
"willTimeOutText": "(i se va lua controlul dacă este inactiv)" "willTimeOutText": "(i se va lua controlul dacă este inactiv)"
}, },
"inventoryText": "Inventar",
"jumpBoldText": "SARI", "jumpBoldText": "SARI",
"jumpText": "Sari", "jumpText": "Sari",
"keepText": "Păstrează", "keepText": "Păstrează",
@ -1022,7 +1036,9 @@
"seasonEndsMinutesText": "Sezonul se sfârșește în ${NUMBER} (de) minute.", "seasonEndsMinutesText": "Sezonul se sfârșește în ${NUMBER} (de) minute.",
"seasonText": "Sezonul ${NUMBER}", "seasonText": "Sezonul ${NUMBER}",
"tournamentLeagueText": "Trebuie să ajungi în liga ${NAME} dacă vrei să intri în acest turneu.", "tournamentLeagueText": "Trebuie să ajungi în liga ${NAME} dacă vrei să intri în acest turneu.",
"trophyCountsResetText": "Numărul de trofee se va reseta la sfârșitul sezonului." "trophyCountsResetText": "Numărul de trofee se va reseta la sfârșitul sezonului.",
"upToDateBonusDescriptionText": "Jucătorii care au o versiune recentă a jocului \nprimesc aici un bonus de ${PERCENT}%.",
"upToDateBonusText": "Bonus la curent"
}, },
"learnMoreText": "Află mai multe", "learnMoreText": "Află mai multe",
"levelBestScoresText": "Cele mai bune scoruri din ${LEVEL}", "levelBestScoresText": "Cele mai bune scoruri din ${LEVEL}",
@ -1070,6 +1086,8 @@
"modeArcadeText": "Mod Pentru Arcade", "modeArcadeText": "Mod Pentru Arcade",
"modeClassicText": "Modul Clasic", "modeClassicText": "Modul Clasic",
"modeDemoText": "Modul Demonstrativ", "modeDemoText": "Modul Demonstrativ",
"moreSoonText": "Mai multe in curând...",
"mostDestroyedPlayerText": "Cel mai distrus jucător",
"mostValuablePlayerText": "Cel mai valoros jucător este", "mostValuablePlayerText": "Cel mai valoros jucător este",
"mostViolatedPlayerText": "Cel mai ucis jucător este", "mostViolatedPlayerText": "Cel mai ucis jucător este",
"mostViolentPlayerText": "Cel mai violent jucător este", "mostViolentPlayerText": "Cel mai violent jucător este",
@ -1120,6 +1138,9 @@
"onText": "Pornit", "onText": "Pornit",
"oneMomentText": "Un Moment...", "oneMomentText": "Un Moment...",
"onslaughtRespawnText": "${PLAYER} va reveni în valul ${WAVE}", "onslaughtRespawnText": "${PLAYER} va reveni în valul ${WAVE}",
"openMeText": "Deschide-mă!",
"openNowText": "Deschide Acum",
"openText": "Deschide",
"orText": "${A} sau ${B}", "orText": "${A} sau ${B}",
"otherText": "Altele...", "otherText": "Altele...",
"outOfText": "(#${RANK} din ${ALL})", "outOfText": "(#${RANK} din ${ALL})",
@ -1251,6 +1272,7 @@
"version_mismatch": "Versiuni Nepotrivite.\nAsigură-te că BombSquad și BombSquad Remote\nau cea mai nouă versiune și încearcă din nou." "version_mismatch": "Versiuni Nepotrivite.\nAsigură-te că BombSquad și BombSquad Remote\nau cea mai nouă versiune și încearcă din nou."
}, },
"removeInGameAdsText": "Deblochează \"${PRO}\" în magazin pentru a şterge reclamele din joc.", "removeInGameAdsText": "Deblochează \"${PRO}\" în magazin pentru a şterge reclamele din joc.",
"removeInGameAdsTokenPurchaseText": "OFERTĂ LIMITATĂ ÎN TIMP: Cumpără ORICE pachet de jetoane pentru a elimina reclamele din joc.",
"renameText": "Redenumeşte", "renameText": "Redenumeşte",
"replayEndText": "Sfârşeşte Reluarea", "replayEndText": "Sfârşeşte Reluarea",
"replayNameDefaultText": "Reluarea Ultimului Joc", "replayNameDefaultText": "Reluarea Ultimului Joc",
@ -1374,6 +1396,7 @@
}, },
"spaceKeyText": "spacebar", "spaceKeyText": "spacebar",
"statsText": "Statistici", "statsText": "Statistici",
"stopRemindingMeText": "Nu-mi Mai Reaminti",
"storagePermissionAccessText": "Acest beneficiu are nevoie de permisiuni de stocare", "storagePermissionAccessText": "Acest beneficiu are nevoie de permisiuni de stocare",
"store": { "store": {
"alreadyOwnText": "Ai cumpărat ${NAME} deja!", "alreadyOwnText": "Ai cumpărat ${NAME} deja!",
@ -1435,6 +1458,7 @@
"testBuildValidatedText": "Versiune de Test validată; bucură-te de ea!", "testBuildValidatedText": "Versiune de Test validată; bucură-te de ea!",
"thankYouText": "Îți mulțumesc pentru suport! Bucură-te de joc!!", "thankYouText": "Îți mulțumesc pentru suport! Bucură-te de joc!!",
"threeKillText": "TRIPLU-OMOR!!", "threeKillText": "TRIPLU-OMOR!!",
"ticketsDescriptionText": "Jetoanele pot fi folosite pentru a debloca caractere, mape, minigame-uri, și altele\nJetoanele pot fi găsite în cufere câștigate din campananii, campionate, și realizări.",
"timeBonusText": "Timp Bonus", "timeBonusText": "Timp Bonus",
"timeElapsedText": "Timp Scurs", "timeElapsedText": "Timp Scurs",
"timeExpiredText": "Timp Expirat", "timeExpiredText": "Timp Expirat",
@ -1449,17 +1473,20 @@
"getTokensText": "Obțineți jetoane", "getTokensText": "Obțineți jetoane",
"notEnoughTokensText": "Insuficiente jetoane!", "notEnoughTokensText": "Insuficiente jetoane!",
"numTokensText": "${COUNT} Jetoane", "numTokensText": "${COUNT} Jetoane",
"openNowDescriptionText": "Ai destule jetoane să\ndeschizi asta acum - nu mai\ntrebuie să aștepți.",
"shinyNewCurrencyText": "Noua monedă strălucitoare a BombSquad-ului.", "shinyNewCurrencyText": "Noua monedă strălucitoare a BombSquad-ului.",
"tokenPack1Text": "Pachet mic de jetoane", "tokenPack1Text": "Pachet mic de jetoane",
"tokenPack2Text": "Pachet mediu de jetoane", "tokenPack2Text": "Pachet mediu de jetoane",
"tokenPack3Text": "Pachet mare de jetoane", "tokenPack3Text": "Pachet mare de jetoane",
"tokenPack4Text": "Pachet de jetoane Jumbo", "tokenPack4Text": "Pachet de jetoane Jumbo",
"tokensDescriptionText": "Jetoanele sunt folosite pentru a accelera deblocarea cuferelor și pentru alte funcții ale jocului și contului. \nPoți câștiga jetoane în joc sau le poți cumpăra în pachete. Sau poți cumpăra un Gold Pass pentru jetoane infinite și nu vei mai auzi niciodată de ele.",
"youHaveGoldPassText": "Ai un permis de aur.\nToate achizițiile de jetoane sunt gratuite.\nBucurați-vă!" "youHaveGoldPassText": "Ai un permis de aur.\nToate achizițiile de jetoane sunt gratuite.\nBucurați-vă!"
}, },
"topFriendsText": "Prieteni de Top", "topFriendsText": "Prieteni de Top",
"tournamentCheckingStateText": "Se verifică starea campionatului; aşteaptă...", "tournamentCheckingStateText": "Se verifică starea campionatului; aşteaptă...",
"tournamentEndedText": "Acest turneu s-a terminat. Altul nou va începe în curând.", "tournamentEndedText": "Acest turneu s-a terminat. Altul nou va începe în curând.",
"tournamentEntryText": "Prețul Pentru Intrare", "tournamentEntryText": "Prețul Pentru Intrare",
"tournamentFinalStandingsText": "Clasamentul final",
"tournamentResultsRecentText": "Rezultatele Recente ale Turneului", "tournamentResultsRecentText": "Rezultatele Recente ale Turneului",
"tournamentStandingsText": "Clasamentele Turneului", "tournamentStandingsText": "Clasamentele Turneului",
"tournamentText": "Turneu", "tournamentText": "Turneu",
@ -1515,6 +1542,18 @@
"Uber Onslaught": "MEGA Măcel", "Uber Onslaught": "MEGA Măcel",
"Uber Runaround": "MEGA Runaround" "Uber Runaround": "MEGA Runaround"
}, },
"displayItemNames": {
"${C} Tickets": "${C} Tichete",
"${C} Tokens": "${C} Jetoane",
"Chest": "Cufăr",
"L1 Chest": "L1 Cufăr",
"L2 Chest": "L2 Cufăr",
"L3 Chest": "L3 Cufăr",
"L4 Chest": "L4 Cufăr",
"L5 Chest": "L5 Cufăr",
"L6 Chest": "L6 Cufăr",
"Unknown Chest": "Cufăr Necunoscut"
},
"gameDescriptions": { "gameDescriptions": {
"Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Fii cel ales pentru o perioadă de timp pentru a câştiga.\nOmoară alesul pentru a deveni tu acela.", "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Fii cel ales pentru o perioadă de timp pentru a câştiga.\nOmoară alesul pentru a deveni tu acela.",
"Bomb as many targets as you can.": "Bombează cât mai multe ținte.", "Bomb as many targets as you can.": "Bombează cât mai multe ținte.",
@ -1598,7 +1637,8 @@
"Arabic": "Arabă", "Arabic": "Arabă",
"Belarussian": "Belarusă", "Belarussian": "Belarusă",
"Chinese": "Chineză (Simplificată)", "Chinese": "Chineză (Simplificată)",
"ChineseTraditional": "Chineză (Tradițională)", "ChineseSimplified": "Chineză - Simplificată",
"ChineseTraditional": "Chineză - Tradițională",
"Croatian": "Croată", "Croatian": "Croată",
"Czech": "Cehă", "Czech": "Cehă",
"Danish": "Daneză", "Danish": "Daneză",
@ -1622,11 +1662,15 @@
"PirateSpeak": "Limba Piraților", "PirateSpeak": "Limba Piraților",
"Polish": "Poloneză", "Polish": "Poloneză",
"Portuguese": "Portugheză", "Portuguese": "Portugheză",
"PortugueseBrazil": "Portugheză - Brazilia",
"PortuguesePortugal": "Portugheză - Portugalia",
"Romanian": "Română", "Romanian": "Română",
"Russian": "Rusă", "Russian": "Rusă",
"Serbian": "Sârbă", "Serbian": "Sârbă",
"Slovak": "Slovacă", "Slovak": "Slovacă",
"Spanish": "Spaniolă", "Spanish": "Spaniolă",
"SpanishLatinAmerica": "Spaniolă - America Latină",
"SpanishSpain": "Spaniolă - Spania",
"Swedish": "Suedeză", "Swedish": "Suedeză",
"Tamil": "Tamilă", "Tamil": "Tamilă",
"Thai": "Thailandeză", "Thai": "Thailandeză",
@ -1691,6 +1735,7 @@
"Cheating detected; scores and prizes suspended for ${COUNT} days.": "Ai fost detectat trișând; scorurile şi premiile vor fi suspendate timp de ${COUNT} (de) zile.", "Cheating detected; scores and prizes suspended for ${COUNT} days.": "Ai fost detectat trișând; scorurile şi premiile vor fi suspendate timp de ${COUNT} (de) zile.",
"Could not establish a secure connection.": "Nu s-a putut stabili o conexiune sigură.", "Could not establish a secure connection.": "Nu s-a putut stabili o conexiune sigură.",
"Daily maximum reached.": "Limită Zilnică Atinsă.", "Daily maximum reached.": "Limită Zilnică Atinsă.",
"Daily sign-in reward": "Recompensă zilnică pentru conectare",
"Entering tournament...": "Se intră în turneu...", "Entering tournament...": "Se intră în turneu...",
"Invalid code.": "Cod Invalid.", "Invalid code.": "Cod Invalid.",
"Invalid payment; purchase canceled.": "Plată invalidă; achiziționare anulată.", "Invalid payment; purchase canceled.": "Plată invalidă; achiziționare anulată.",
@ -1700,11 +1745,14 @@
"Item unlocked!": "Lucru deblocat!", "Item unlocked!": "Lucru deblocat!",
"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)": "CONECTARE ÎNTRE CONTURI RESPINSĂ.Contul ${ACCOUNT} conține\ndate semnificative care ar putea FI PIERDUTE PE VECI.\nPoți să conectezi conturile în ordinea opusă acesteia dacă dorești\n(și să pierzi datele ACESTUI cont în locul celuilalt)", "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)": "CONECTARE ÎNTRE CONTURI RESPINSĂ.Contul ${ACCOUNT} conține\ndate semnificative care ar putea FI PIERDUTE PE VECI.\nPoți să conectezi conturile în ordinea opusă acesteia dacă dorești\n(și să pierzi datele ACESTUI cont în locul celuilalt)",
"Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "Îți legi progresul contului ${ACCOUNT} cu acest cont?\nTot progresul de pe ${ACCOUNT} va fi șters.\nAcest lucru nu poate fi reversibil. Ești sigur?", "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "Îți legi progresul contului ${ACCOUNT} cu acest cont?\nTot progresul de pe ${ACCOUNT} va fi șters.\nAcest lucru nu poate fi reversibil. Ești sigur?",
"Longer streaks lead to better rewards.": "Serii mai lungi duc la recompense mai bune.",
"Max number of playlists reached.": "Numărul maxim de liste a fost atins.", "Max number of playlists reached.": "Numărul maxim de liste a fost atins.",
"Max number of profiles reached.": "Numărul maxim de profile a fost atins.", "Max number of profiles reached.": "Numărul maxim de profile a fost atins.",
"Maximum friend code rewards reached.": "Limita codurilor pentru prieteni a fost atinsă.", "Maximum friend code rewards reached.": "Limita codurilor pentru prieteni a fost atinsă.",
"Message is too long.": "Mesajul este prea lung.", "Message is too long.": "Mesajul este prea lung.",
"New tournament result!": "Noul rezultat al campionatului!",
"No servers are available. Please try again soon.": "Niciun server nu este disponibil. Încearcă din nou mai târziu.", "No servers are available. Please try again soon.": "Niciun server nu este disponibil. Încearcă din nou mai târziu.",
"No slots available. Free a slot and try again.": "Nu există sloturi disponibile. Eliberați un slot și încercați din nou.",
"Profile \"${NAME}\" upgraded successfully.": "Profilul \"${NAME}\" a fost îmbunătățit cu succes.", "Profile \"${NAME}\" upgraded successfully.": "Profilul \"${NAME}\" a fost îmbunătățit cu succes.",
"Profile could not be upgraded.": "Profilul nu a putut fi îmbunătățit.", "Profile could not be upgraded.": "Profilul nu a putut fi îmbunătățit.",
"Purchase successful!": "Achiziționare reuşită!", "Purchase successful!": "Achiziționare reuşită!",
@ -1714,7 +1762,9 @@
"Sorry, this code has already been used.": "Scuze, dar codul acesta a fost deja folosit.", "Sorry, this code has already been used.": "Scuze, dar codul acesta a fost deja folosit.",
"Sorry, this code has expired.": "Scuze, dar acest cod a expirat.", "Sorry, this code has expired.": "Scuze, dar acest cod a expirat.",
"Sorry, this code only works for new accounts.": "Scuze, dar acest cod merge numai pentru jucătorii cu conturi noi.", "Sorry, this code only works for new accounts.": "Scuze, dar acest cod merge numai pentru jucătorii cu conturi noi.",
"Sorry, this has expired.": "Scuze, acest lucru a expirat.",
"Still searching for nearby servers; please try again soon.": "Încă se caută servere din apropiere; te rog să încerci din nou într-un moment.", "Still searching for nearby servers; please try again soon.": "Încă se caută servere din apropiere; te rog să încerci din nou într-un moment.",
"Streak: ${NUM} days": "Serie: ${NUM} zile",
"Temporarily unavailable; please try again later.": "Nevalabil temporar; te rog să încerci din nou mai târziu.", "Temporarily unavailable; please try again later.": "Nevalabil temporar; te rog să încerci din nou mai târziu.",
"The tournament ended before you finished.": "Turneul s-a sfârşit înainte să-l termini.", "The tournament ended before you finished.": "Turneul s-a sfârşit înainte să-l termini.",
"This account cannot be unlinked for ${NUM} days.": "Acest cont nu poate fi deconectat timp de ${NUM} (de) zile.", "This account cannot be unlinked for ${NUM} days.": "Acest cont nu poate fi deconectat timp de ${NUM} (de) zile.",
@ -1725,20 +1775,28 @@
"Tournaments require ${VERSION} or newer": "Turneele sunt valabile de la versiunea ${VERSION} în sus", "Tournaments require ${VERSION} or newer": "Turneele sunt valabile de la versiunea ${VERSION} în sus",
"Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "Deconectezi contul ${ACCOUNT} de la acest cont?\nTot progresul de pe ${ACCOUNT} va fi resetat.\n(exceptând realizările în unele cazuri)", "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "Deconectezi contul ${ACCOUNT} de la acest cont?\nTot progresul de pe ${ACCOUNT} va fi resetat.\n(exceptând realizările în unele cazuri)",
"WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "ADVERTISMENT: Reclamații cum că ai fost acuzat de trișat/hackuit au fost legate de contul tău.\nConturile persoanelor prinse trișând/hackuind vor fi restricționate. Te rog să joci cinstit.", "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "ADVERTISMENT: Reclamații cum că ai fost acuzat de trișat/hackuit au fost legate de contul tău.\nConturile persoanelor prinse trișând/hackuind vor fi restricționate. Te rog să joci cinstit.",
"Wait reduced!": "Aștepare redusă!",
"Warning: This version of the game is limited to old account data; things may appear missing or out of date.\nPlease upgrade to a newer version of the game to see your latest account data.": "Atenție: Această versiune a jocului este limitată la datele vechi ale contului; este posibil să apară informații care lipsesc sau care sunt învechite.\n Te rugăm să treci la o versiune mai nouă a jocului pentru a vedea cele mai recente date ale contului tău.",
"Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "Ai dori să îți conectezi contul tău de pe dispozitiv cu acesta?\n\nContul tău de pe dispozitiv este ${ACCOUNT1}\nAcest cont este ${ACCOUNT2}\n\nAcest lucru îți va salva progresul existent.\nAtenție: acest lucru nu poate fi reversibil!", "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "Ai dori să îți conectezi contul tău de pe dispozitiv cu acesta?\n\nContul tău de pe dispozitiv este ${ACCOUNT1}\nAcest cont este ${ACCOUNT2}\n\nAcest lucru îți va salva progresul existent.\nAtenție: acest lucru nu poate fi reversibil!",
"You already own this!": "Deja ai acest lucru!", "You already own this!": "Deja ai acest lucru!",
"You can join in ${COUNT} seconds.": "Poți reintra în ${COUNT} secunde.", "You can join in ${COUNT} seconds.": "Poți reintra în ${COUNT} secunde.",
"You don't have enough tickets for this!": "Nu ai destule bilete pentru acest lucru!", "You don't have enough tickets for this!": "Nu ai destule bilete pentru acest lucru!",
"You don't own that.": "Nu deți asta.", "You don't own that.": "Nu deți asta.",
"You got ${COUNT} tickets!": "Ai primit ${COUNT} (de) bilete!", "You got ${COUNT} tickets!": "Ai primit ${COUNT} (de) bilete!",
"You got ${COUNT} tokens!": "Ai primit ${COUNT} jetoane!",
"You got a ${ITEM}!": "Ai primit 1 ${ITEM}!", "You got a ${ITEM}!": "Ai primit 1 ${ITEM}!",
"You got a chest!": "Ai primit un cufăr!",
"You got an achievement reward!": "Ai primit o recompensă a unei realizări!",
"You have been promoted to a new league; congratulations!": "Ai fost promovat la o ligă noua; felicitări!", "You have been promoted to a new league; congratulations!": "Ai fost promovat la o ligă noua; felicitări!",
"You lost a chest! (All your chest slots were full)": "Ai pierdut un cufăr! (Toate sloturile pentru cufere erau pline)",
"You must update the app to view this.": "Trebuie să actualizezi aplicația pentru a vedea asta.", "You must update the app to view this.": "Trebuie să actualizezi aplicația pentru a vedea asta.",
"You must update to a newer version of the app to do this.": "Trebuie să-ți actualizezi aplicația la o versiune mai nouă pentru a face asta.", "You must update to a newer version of the app to do this.": "Trebuie să-ți actualizezi aplicația la o versiune mai nouă pentru a face asta.",
"You must update to the newest version of the game to do this.": "Trebuie să-ți actualizezi jocul la o versiune mai nouă pentru a face asta.", "You must update to the newest version of the game to do this.": "Trebuie să-ți actualizezi jocul la o versiune mai nouă pentru a face asta.",
"You must wait a few seconds before entering a new code.": "Va trebui să aştepți câteva secunde înainte să introduci un cod nou.", "You must wait a few seconds before entering a new code.": "Va trebui să aştepți câteva secunde înainte să introduci un cod nou.",
"You placed #${RANK} in a tournament!": "Ai fost plasat pe #${RANK} într-un campionat!",
"You ranked #${RANK} in the last tournament. Thanks for playing!": "Ai avut rankul #${RANK} în ultimul campionat. Mulțumesc pentru participare!", "You ranked #${RANK} in the last tournament. Thanks for playing!": "Ai avut rankul #${RANK} în ultimul campionat. Mulțumesc pentru participare!",
"Your account was rejected. Are you signed in?": "Contul tău a fost respins. Ești conectat?", "Your account was rejected. Are you signed in?": "Contul tău a fost respins. Ești conectat?",
"Your ad views are not registering. Ad options will be limited for a while.": "Vizionările tale de reclame nu sunt înregistrate. Opțiunile pentru reclame vor fi limitate pentru o perioadă.",
"Your copy of the game has been modified.\nPlease revert any changes and try again.": "Copia ta de joc deținută a fost modificată.\nTe rog să treci la normal orice schimbare făcută şi să încerci din nou.", "Your copy of the game has been modified.\nPlease revert any changes and try again.": "Copia ta de joc deținută a fost modificată.\nTe rog să treci la normal orice schimbare făcută şi să încerci din nou.",
"Your friend code was used by ${ACCOUNT}": "${ACCOUNT} a folosit codul tău de prieten" "Your friend code was used by ${ACCOUNT}": "${ACCOUNT} a folosit codul tău de prieten"
}, },
@ -1890,6 +1948,7 @@
"twoKillText": "DUBLU OMOR!", "twoKillText": "DUBLU OMOR!",
"uiScaleText": "Mărime Interfață", "uiScaleText": "Mărime Interfață",
"unavailableText": "indisponibil", "unavailableText": "indisponibil",
"unclaimedPrizesText": "Ai premii ne revendicate!",
"unconfiguredControllerDetectedText": "Controller neconfigurat detectat:", "unconfiguredControllerDetectedText": "Controller neconfigurat detectat:",
"unlockThisInTheStoreText": "Acest lucru trebuie deblocat din magazin.", "unlockThisInTheStoreText": "Acest lucru trebuie deblocat din magazin.",
"unlockThisProfilesText": "Că să creezi mai mult de ${NUM} profile, va trebui să ai:", "unlockThisProfilesText": "Că să creezi mai mult de ${NUM} profile, va trebui să ai:",
@ -1976,5 +2035,6 @@
}, },
"yesAllowText": "Da, Permite!", "yesAllowText": "Da, Permite!",
"yourBestScoresText": "Cele Mai Bune Scoruri Ale Tale", "yourBestScoresText": "Cele Mai Bune Scoruri Ale Tale",
"yourBestTimesText": "Cei Mai Buni Timpi Ai Tăi" "yourBestTimesText": "Cei Mai Buni Timpi Ai Tăi",
"yourPrizeText": "Premiul tău:"
} }

View file

@ -1725,6 +1725,7 @@
"Indonesian": "Индонезийский", "Indonesian": "Индонезийский",
"Italian": "Итальянский", "Italian": "Итальянский",
"Japanese": "Японский", "Japanese": "Японский",
"Kazakh": "казахский",
"Korean": "Корейский", "Korean": "Корейский",
"Malay": "Малайский", "Malay": "Малайский",
"Persian": "Персидский", "Persian": "Персидский",
@ -1861,6 +1862,7 @@
"You got an achievement reward!": "Вы получили награду за достижение!", "You got an achievement reward!": "Вы получили награду за достижение!",
"You have been promoted to a new league; congratulations!": "Вас повысили и перевели в новую лигу; поздравляем!", "You have been promoted to a new league; congratulations!": "Вас повысили и перевели в новую лигу; поздравляем!",
"You lost a chest! (All your chest slots were full)": "Вы потеряли сундук! (Ваш инвентарь полон)", "You lost a chest! (All your chest slots were full)": "Вы потеряли сундук! (Ваш инвентарь полон)",
"You must sign in to do this.": "Вы должны войти, чтобы сделать это.",
"You must update the app to view this.": "Вы должны обновить игру чтобы увидеть это.", "You must update the app to view this.": "Вы должны обновить игру чтобы увидеть это.",
"You must update to a newer version of the app to do this.": "Чтобы это сделать, вы должны обновить приложение.", "You must update to a newer version of the app to do this.": "Чтобы это сделать, вы должны обновить приложение.",
"You must update to the newest version of the game to do this.": "Вы должны обновиться до новейшей версии игры, чтобы сделать это.", "You must update to the newest version of the game to do this.": "Вы должны обновиться до новейшей версии игры, чтобы сделать это.",
@ -1939,7 +1941,7 @@
"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..": "Не стой на месте помрешь. Беги и уворачивайся чтобы выжить..",

View file

@ -7,6 +7,7 @@
"campaignProgressText": "Priebeh kampane [Hard] : ${PROGRESS}", "campaignProgressText": "Priebeh kampane [Hard] : ${PROGRESS}",
"changeOncePerSeason": "Toto môžete zmeniť len raz za sezónu.", "changeOncePerSeason": "Toto môžete zmeniť len raz za sezónu.",
"changeOncePerSeasonError": "Musíte počkať do ďalšej sezóny aby ste mohli toto znova zmeniť (${NUM} days)", "changeOncePerSeasonError": "Musíte počkať do ďalšej sezóny aby ste mohli toto znova zmeniť (${NUM} days)",
"createAnAccountText": "Vytvoriť si Účet",
"customName": "Vlastný názov", "customName": "Vlastný názov",
"deleteAccountText": "Zmazať Účet", "deleteAccountText": "Zmazať Účet",
"googlePlayGamesAccountSwitchText": "Ak chcete použiť iný účet Google,\nna jeho výmenu použite aplikáciu hry Google Play.", "googlePlayGamesAccountSwitchText": "Ak chcete použiť iný účet Google,\nna jeho výmenu použite aplikáciu hry Google Play.",
@ -377,7 +378,16 @@
"chatMuteText": "Zablokovať Čet", "chatMuteText": "Zablokovať Čet",
"chatMutedText": "Čet Zablokovaný", "chatMutedText": "Čet Zablokovaný",
"chatUnMuteText": "Odblokovať Čet", "chatUnMuteText": "Odblokovať Čet",
"chests": {
"prizeOddsText": "Šance Odmien",
"reduceWaitText": "Skrátiť Čakanie",
"slotDescriptionText": "Toto miesto slúži na truhlicu.\n\nZískaj truhlice hraním kampane,\numiestnením sa v turnajoch a\nplnením achievementov.",
"slotText": "Slot na Truhlicu ${NUM}",
"slotsFullWarningText": "VAROVANIE: Všetky tvoje sloty na truhlice sú plné.\nAkékoľvek truhlice čo túto hru získaš budú stratené.",
"unlocksInText": "Odomkne Sa Za"
},
"choosingPlayerText": "<prebieha vyberanie postavy>", "choosingPlayerText": "<prebieha vyberanie postavy>",
"claimText": "Získať",
"codesExplainText": "Kódy sú poskytované vývojárovi \nk diagnostike a oprave problémov s účtom.", "codesExplainText": "Kódy sú poskytované vývojárovi \nk diagnostike a oprave problémov s účtom.",
"completeThisLevelToProceedText": "Musíš dokončiť\ntento level pre postup!", "completeThisLevelToProceedText": "Musíš dokončiť\ntento level pre postup!",
"completionBonusText": "Bonus za Dokončenie", "completionBonusText": "Bonus za Dokončenie",
@ -657,6 +667,8 @@
"errorText": "Chyba", "errorText": "Chyba",
"errorUnknownText": "neznámy error", "errorUnknownText": "neznámy error",
"exitGameText": "Ukončiť ${APP_NAME}?", "exitGameText": "Ukončiť ${APP_NAME}?",
"expiredAgoText": "Vypršalo pred ${T}",
"expiresInText": "Vyprší za ${T}",
"exportSuccessText": "\"${NAME}\" exportovaný.", "exportSuccessText": "\"${NAME}\" exportovaný.",
"externalStorageText": "Externé Úložisko", "externalStorageText": "Externé Úložisko",
"failText": "Zlyhanie", "failText": "Zlyhanie",
@ -718,6 +730,7 @@
"copyCodeConfirmText": "Kód bol skopírovaný do schránky.", "copyCodeConfirmText": "Kód bol skopírovaný do schránky.",
"copyCodeText": "Kopírovať kód", "copyCodeText": "Kopírovať kód",
"dedicatedServerInfoText": "Pre najlepšie výsledky, nastav si dedikovaný server. Pozri bombsquadgame.com/server a nauč sa ako.", "dedicatedServerInfoText": "Pre najlepšie výsledky, nastav si dedikovaný server. Pozri bombsquadgame.com/server a nauč sa ako.",
"descriptionShortText": "Použi okno Viac Hráčov na vytvorenie párty.",
"disconnectClientsText": "Toto odpojí ${COUNT} hráča/hráčov v tvojej\npárty. Si si istý?", "disconnectClientsText": "Toto odpojí ${COUNT} hráča/hráčov v tvojej\npárty. Si si istý?",
"earnTicketsForRecommendingAmountText": "Kamaráti dostanú ${COUNT} tiketov a si hru vyskúšajú (a\nza každého kto tak urobí dostaneš ${YOU_COUNT} tiketov)", "earnTicketsForRecommendingAmountText": "Kamaráti dostanú ${COUNT} tiketov a si hru vyskúšajú (a\nza každého kto tak urobí dostaneš ${YOU_COUNT} tiketov)",
"earnTicketsForRecommendingText": "Zdieľaj hru pre\ntikety zadarmo...", "earnTicketsForRecommendingText": "Zdieľaj hru pre\ntikety zadarmo...",
@ -730,7 +743,7 @@
"friendHasSentPromoCodeText": "${COUNT} ${APP_NAME} tiketov od ${NAME}", "friendHasSentPromoCodeText": "${COUNT} ${APP_NAME} tiketov od ${NAME}",
"friendPromoCodeAwardText": "Dostaneš ${COUNT} tiketov každý raz keď sa použie.", "friendPromoCodeAwardText": "Dostaneš ${COUNT} tiketov každý raz keď sa použie.",
"friendPromoCodeExpireText": "Kód vyprší za ${EXPIRE_HOURS} hodín a funguje len pre nové účty.", "friendPromoCodeExpireText": "Kód vyprší za ${EXPIRE_HOURS} hodín a funguje len pre nové účty.",
"friendPromoCodeInstructionsText": "Ak ho chceš použiť, otvor ${APP_NAME} a choď do \"Settings->Advanced->Enter Code\".\nPozri bombsquadgame.com pre download linky pre všetky podporované platformy.", "friendPromoCodeInstructionsText": "Ak ho chceš použiť, otvor ${APP_NAME} a choď do \"Nastavenia->pokročilé->Poslať Info\".\nPozri bombsquadgame.com pre download linky pre všetky podporované platformy.",
"friendPromoCodeRedeemLongText": "Môže byť uplatnený za ${COUNT} tiketov až pre ${MAX_USES} ľudí.", "friendPromoCodeRedeemLongText": "Môže byť uplatnený za ${COUNT} tiketov až pre ${MAX_USES} ľudí.",
"friendPromoCodeRedeemShortText": "Môže byť uplatnený za ${COUNT} tiketov v hre.", "friendPromoCodeRedeemShortText": "Môže byť uplatnený za ${COUNT} tiketov v hre.",
"friendPromoCodeWhereToEnterText": "(V časti „Nastavenia-> Pokročilé-> Poslať info\")", "friendPromoCodeWhereToEnterText": "(V časti „Nastavenia-> Pokročilé-> Poslať info\")",
@ -909,6 +922,7 @@
"importText": "Importovať", "importText": "Importovať",
"importingText": "Importujem...", "importingText": "Importujem...",
"inGameClippedNameText": "v hre to bude\n\"${NAME}\"", "inGameClippedNameText": "v hre to bude\n\"${NAME}\"",
"inboxText": "Správy",
"installDiskSpaceErrorText": "ERROR: Nemožno dokončiť inštaláciu.\nAsi budeš mať málo miesta na úložisku.\nTrocha ho vyčisti a skús to znova.", "installDiskSpaceErrorText": "ERROR: Nemožno dokončiť inštaláciu.\nAsi budeš mať málo miesta na úložisku.\nTrocha ho vyčisti a skús to znova.",
"internal": { "internal": {
"arrowsToExitListText": "stlač ${LEFT} alebo ${RIGHT} pre zatvorenie listu", "arrowsToExitListText": "stlač ${LEFT} alebo ${RIGHT} pre zatvorenie listu",
@ -963,12 +977,14 @@
"timeOutText": "(vyprší za ${TIME} sekúnd)", "timeOutText": "(vyprší za ${TIME} sekúnd)",
"touchScreenJoinWarningText": "Pripojil si sa na obrazovke.\nAk to bolo nechtiac, stlač na obrazovke Menu->Opustiť Hru.", "touchScreenJoinWarningText": "Pripojil si sa na obrazovke.\nAk to bolo nechtiac, stlač na obrazovke Menu->Opustiť Hru.",
"touchScreenText": "Obrazovka", "touchScreenText": "Obrazovka",
"unableToCompleteTryAgainText": "Nemožno to teraz dokončiť.\nSkúste to znova.",
"unableToResolveHostText": "Error: žiadny internet/zlé host meno.", "unableToResolveHostText": "Error: žiadny internet/zlé host meno.",
"unavailableNoConnectionText": "Toto nie je aktuálne dostupné (žiadny internet?)", "unavailableNoConnectionText": "Toto nie je aktuálne dostupné (žiadny internet?)",
"vrOrientationResetCardboardText": "Toto použi pre resetovanie orientácie VR.\nAk chceš hrať, potrebuješ ovládač.", "vrOrientationResetCardboardText": "Toto použi pre resetovanie orientácie VR.\nAk chceš hrať, potrebuješ ovládač.",
"vrOrientationResetText": "Orientácia VR resetovaná.", "vrOrientationResetText": "Orientácia VR resetovaná.",
"willTimeOutText": "(ak bude nečinný, vyprší čas)" "willTimeOutText": "(ak bude nečinný, vyprší čas)"
}, },
"inventoryText": "Inventár",
"jumpBoldText": "SKOČIŤ", "jumpBoldText": "SKOČIŤ",
"jumpText": "Skočiť", "jumpText": "Skočiť",
"keepText": "Ponechať", "keepText": "Ponechať",
@ -1015,8 +1031,11 @@
"seasonEndsMinutesText": "Sezóna skončí za ${NUMBER} minút.", "seasonEndsMinutesText": "Sezóna skončí za ${NUMBER} minút.",
"seasonText": "Sezóna ${NUMBER}", "seasonText": "Sezóna ${NUMBER}",
"tournamentLeagueText": "Musíš dosiahnuť ${NAME} ligu aby si mohol hrať tento turnaj.", "tournamentLeagueText": "Musíš dosiahnuť ${NAME} ligu aby si mohol hrať tento turnaj.",
"trophyCountsResetText": "Trofeje sa resetujú ďalšiu sezónu." "trophyCountsResetText": "Trofeje sa resetujú ďalšiu sezónu.",
"upToDateBonusDescriptionText": "Hráči s aktuálnou verziou hry\ntu dostanú ${PERCENT}% bonus.",
"upToDateBonusText": "Bonus za aktuálnu verziu hry."
}, },
"learnMoreText": "Zistiť Viac",
"levelBestScoresText": "Najlepšie skóre na ${LEVEL}", "levelBestScoresText": "Najlepšie skóre na ${LEVEL}",
"levelBestTimesText": "Najlepšie časy na ${LEVEL}", "levelBestTimesText": "Najlepšie časy na ${LEVEL}",
"levelIsLockedText": "${LEVEL} je zamknutý.", "levelIsLockedText": "${LEVEL} je zamknutý.",
@ -1060,6 +1079,8 @@
"modeArcadeText": "Arkádový režim", "modeArcadeText": "Arkádový režim",
"modeClassicText": "Klasický režim", "modeClassicText": "Klasický režim",
"modeDemoText": "Demo režim", "modeDemoText": "Demo režim",
"moreSoonText": "Viac už čoskoro...",
"mostDestroyedPlayerText": "Najviac Zabitý Hráč",
"mostValuablePlayerText": "Najcennejší Hráč", "mostValuablePlayerText": "Najcennejší Hráč",
"mostViolatedPlayerText": "Najzomierajúcejší Hráč", "mostViolatedPlayerText": "Najzomierajúcejší Hráč",
"mostViolentPlayerText": "Najvražednejší Hráč", "mostViolentPlayerText": "Najvražednejší Hráč",
@ -1096,6 +1117,7 @@
"noValidMapsErrorText": "Žiadne platné mapy sa pre tento typ hry nenašli.", "noValidMapsErrorText": "Žiadne platné mapy sa pre tento typ hry nenašli.",
"notEnoughPlayersRemainingText": "Nedostatok hráčov; začni novú hru.", "notEnoughPlayersRemainingText": "Nedostatok hráčov; začni novú hru.",
"notEnoughPlayersText": "Potrebuješ aspoň ${COUNT} hráčov ak chceš toto hrať!", "notEnoughPlayersText": "Potrebuješ aspoň ${COUNT} hráčov ak chceš toto hrať!",
"notEnoughTicketsText": "Nedostatok ticketov!",
"notNowText": "Teraz Nie", "notNowText": "Teraz Nie",
"notSignedInErrorText": "Ak toto chceš urobiť, musíš sa prihlásiť.", "notSignedInErrorText": "Ak toto chceš urobiť, musíš sa prihlásiť.",
"notSignedInGooglePlayErrorText": "Ak chceš toto urobiť, musíš sa prihlásiť do Google Play.", "notSignedInGooglePlayErrorText": "Ak chceš toto urobiť, musíš sa prihlásiť do Google Play.",
@ -1108,6 +1130,9 @@
"onText": "Zapnúť", "onText": "Zapnúť",
"oneMomentText": "Chvíľu...", "oneMomentText": "Chvíľu...",
"onslaughtRespawnText": "${PLAYER} sa znova zjaví vo vlne ${WAVE}", "onslaughtRespawnText": "${PLAYER} sa znova zjaví vo vlne ${WAVE}",
"openMeText": "Otvor Ma!",
"openNowText": "Otvoriť Teraz",
"openText": "Otvoriť",
"orText": "${A} alebo ${B}", "orText": "${A} alebo ${B}",
"otherText": "Ďalšie...", "otherText": "Ďalšie...",
"outOfText": "(#${RANK} z ${ALL})", "outOfText": "(#${RANK} z ${ALL})",
@ -1193,6 +1218,8 @@
"punchText": "Úder", "punchText": "Úder",
"purchaseForText": "Kúpiť za ${PRICE}", "purchaseForText": "Kúpiť za ${PRICE}",
"purchaseGameText": "Kúpiť Hru", "purchaseGameText": "Kúpiť Hru",
"purchaseNeverAvailableText": "Pardón, platby nie sú dostupné na tejto platforme.\nSkúste sa prihlásiť do svojho účtu na inej platforme a urobte platbu tam.",
"purchaseNotAvailableText": "Táto platba nie je dostupná.",
"purchasingText": "Kupujem...", "purchasingText": "Kupujem...",
"quitGameText": "Ukončiť ${APP_NAME}?", "quitGameText": "Ukončiť ${APP_NAME}?",
"quittingIn5SecondsText": "Ukončujem za 5 sekúnd..", "quittingIn5SecondsText": "Ukončujem za 5 sekúnd..",
@ -1234,6 +1261,7 @@
"version_mismatch": "Nezhoda verzií.\nUisti sa že je Bombsquad aj BSRemote\nna najnovšej verzii." "version_mismatch": "Nezhoda verzií.\nUisti sa že je Bombsquad aj BSRemote\nna najnovšej verzii."
}, },
"removeInGameAdsText": "Odomkni si \"${PRO}\" v obchode pre odstránenie reklám.", "removeInGameAdsText": "Odomkni si \"${PRO}\" v obchode pre odstránenie reklám.",
"removeInGameAdsTokenPurchaseText": "LIMITOVANÁ ČASOVÁ PONUKA: Kúpte HOCIJAKÝ token pack na odstránenie reklám v hre.",
"renameText": "Premenovať", "renameText": "Premenovať",
"replayEndText": "Ukončiť Replay", "replayEndText": "Ukončiť Replay",
"replayNameDefaultText": "Replay Poslednej Hry", "replayNameDefaultText": "Replay Poslednej Hry",
@ -1267,6 +1295,7 @@
}, },
"scoreWasText": "(predtým ${COUNT})", "scoreWasText": "(predtým ${COUNT})",
"selectText": "Vybrať", "selectText": "Vybrať",
"sendInfoDescriptionText": "Pošle účet a info o situácií aplikácie vývojárovi.\nProsím uveďte svoje meno a dôvod posielania.",
"seriesWinLine1PlayerText": "VYHRÁVA", "seriesWinLine1PlayerText": "VYHRÁVA",
"seriesWinLine1TeamText": "VYHRÁVA", "seriesWinLine1TeamText": "VYHRÁVA",
"seriesWinLine1Text": "VYHRÁVA", "seriesWinLine1Text": "VYHRÁVA",
@ -1284,6 +1313,7 @@
"alwaysUseInternalKeyboardDescriptionText": "(jednoduchá, podporujúca-ovládač na-obrazovke-klávesnica pre písanie textu)", "alwaysUseInternalKeyboardDescriptionText": "(jednoduchá, podporujúca-ovládač na-obrazovke-klávesnica pre písanie textu)",
"alwaysUseInternalKeyboardText": "Stále používať klávesnicu v programe", "alwaysUseInternalKeyboardText": "Stále používať klávesnicu v programe",
"benchmarksText": "Benchmarky & Stres-Testy", "benchmarksText": "Benchmarky & Stres-Testy",
"devToolsText": "Nástroje Pre Vývojára",
"disableCameraGyroscopeMotionText": "Zakázať pohyb gyroskopu fotoaparátu", "disableCameraGyroscopeMotionText": "Zakázať pohyb gyroskopu fotoaparátu",
"disableCameraShakeText": "Zakázať otrasy fotoaparátu", "disableCameraShakeText": "Zakázať otrasy fotoaparátu",
"disableThisNotice": "(toto upozornenie môžeš vypnúť v Pokročilých nastaveniach)", "disableThisNotice": "(toto upozornenie môžeš vypnúť v Pokročilých nastaveniach)",
@ -1292,17 +1322,22 @@
"enterPromoCodeText": "Vložiť Kód", "enterPromoCodeText": "Vložiť Kód",
"forTestingText": "Poznámka: tieto čísla sú len pre testovanie a budú stratené keď sa aplikácia vypne.", "forTestingText": "Poznámka: tieto čísla sú len pre testovanie a budú stratené keď sa aplikácia vypne.",
"helpTranslateText": "${APP_NAME} podporuje aj iné jazyky ako Slovenský.\nAk chceš preložiť hru do iného jazyka alebo upraviť\npreklad, klikni na tlačidlo nižšie. Vďaka za pokrok!", "helpTranslateText": "${APP_NAME} podporuje aj iné jazyky ako Slovenský.\nAk chceš preložiť hru do iného jazyka alebo upraviť\npreklad, klikni na tlačidlo nižšie. Vďaka za pokrok!",
"kickIdlePlayersText": "Vyhodiť Nečinných Hráčov", "insecureConnectionsDescriptionText": "neodporúčané, no môže povoliť online hranie\nz obmedzených krajín alebo pripojení.",
"insecureConnectionsText": "Použi nezabezpečené pripojenia",
"kickIdlePlayersText": "Vyhodiť nečinných hráčov",
"kidFriendlyModeText": "Mód pre Deti (znížené násilie, atď.)", "kidFriendlyModeText": "Mód pre Deti (znížené násilie, atď.)",
"languageText": "Jazyk", "languageText": "Jazyk",
"moddingGuideText": "Tutoriál pre Módovanie", "moddingGuideText": "Tutoriál pre Módovanie",
"moddingToolsText": "Módovacie Nástroje",
"mustRestartText": "Ak chceš toto nastavenie použiť, musíš reštartovať hru.", "mustRestartText": "Ak chceš toto nastavenie použiť, musíš reštartovať hru.",
"netTestingText": "Testovanie Internetu", "netTestingText": "Testovanie Internetu",
"resetText": "Resetovať", "resetText": "Resetovať",
"sendInfoText": "Poslať Info",
"showBombTrajectoriesText": "Ukázovať Trajektóriu Bomby", "showBombTrajectoriesText": "Ukázovať Trajektóriu Bomby",
"showDemosWhenIdleText": "Zobraziť Ukážky Pri Nečinnosti", "showDemosWhenIdleText": "Zobraziť ukážky pri nečinnosti",
"showDevConsoleButtonText": "Zobraziť Tlačidlo Konzoly Zariadenia", "showDeprecatedLoginTypesText": "Ukázať zastarané spôsoby prihlásenia",
"showInGamePingText": "Ukázať Ping v Hre", "showDevConsoleButtonText": "Zobraziť tlačidlo konzoly pre vývojára",
"showInGamePingText": "Ukázať ping v hre",
"showPlayerNamesText": "Ukazovať Mená Hráčov", "showPlayerNamesText": "Ukazovať Mená Hráčov",
"showUserModsText": "Ukázať Zložku pre Módy", "showUserModsText": "Ukázať Zložku pre Módy",
"titleText": "Pokročilé", "titleText": "Pokročilé",
@ -1310,8 +1345,8 @@
"translationFetchErrorText": "status pre jazyk nedostupný", "translationFetchErrorText": "status pre jazyk nedostupný",
"translationFetchingStatusText": "kontrolujem status jazyka...", "translationFetchingStatusText": "kontrolujem status jazyka...",
"translationInformMe": "Informuj ma keď môj jazyk potrebuje vylepšiť", "translationInformMe": "Informuj ma keď môj jazyk potrebuje vylepšiť",
"translationNoUpdateNeededText": "Slovenčina je celá preložená, jupiii!", "translationNoUpdateNeededText": "Slovenský preklad je kompletný, jupiii!",
"translationUpdateNeededText": "** Slovenčina potrebuje dokončiť!! **", "translationUpdateNeededText": "** Slovenčina potrebuje dokončiť preklad!! **",
"vrTestingText": "VR Testovanie" "vrTestingText": "VR Testovanie"
}, },
"shareText": "Zdieľať", "shareText": "Zdieľať",
@ -1349,6 +1384,7 @@
}, },
"spaceKeyText": "medzerník", "spaceKeyText": "medzerník",
"statsText": "Štatistiky", "statsText": "Štatistiky",
"stopRemindingMeText": "Prestaň Mi Pripomínať",
"storagePermissionAccessText": "Na toto potrebuješ povolenie k úložisku", "storagePermissionAccessText": "Na toto potrebuješ povolenie k úložisku",
"store": { "store": {
"alreadyOwnText": "Už vlastníš ${NAME}!", "alreadyOwnText": "Už vlastníš ${NAME}!",
@ -1407,6 +1443,7 @@
"testBuildValidatedText": "Testovacia Verzia Platná; Uži si to!", "testBuildValidatedText": "Testovacia Verzia Platná; Uži si to!",
"thankYouText": "Vďaka za podporu! Uži si hru!!", "thankYouText": "Vďaka za podporu! Uži si hru!!",
"threeKillText": "TRIPLE-KILL!!!", "threeKillText": "TRIPLE-KILL!!!",
"ticketsDescriptionText": "Filety môžu byť použité na odomknutie charakterov,\nmáp, minihier a viac v obchode.\n\nTickety sa dajú nájsť v truhliciach vyhrané v\nkampaniach, turnajoch a po dosiahnutí achievementov",
"timeBonusText": "Časový Bonus", "timeBonusText": "Časový Bonus",
"timeElapsedText": "Čas Uplynul", "timeElapsedText": "Čas Uplynul",
"timeExpiredText": "Čas Uplynul", "timeExpiredText": "Čas Uplynul",
@ -1417,10 +1454,24 @@
"tipText": "Tip", "tipText": "Tip",
"titleText": "BombSquad", "titleText": "BombSquad",
"titleVRText": "BombSquad VR", "titleVRText": "BombSquad VR",
"tokens": {
"getTokensText": "Dostaňte tokeny",
"notEnoughTokensText": "Nedostatok tokenov!",
"numTokensText": "${COUNT} Tokenov",
"openNowDescriptionText": "Máš dostatok tokenov\nna otvorenie teraz\n- nemusíš čakať.",
"shinyNewCurrencyText": "Úplne nová mena BombSquadu",
"tokenPack1Text": "Malý Token Balík",
"tokenPack2Text": "Stredný Token Balík",
"tokenPack3Text": "Veľký Token Balík",
"tokenPack4Text": "Obrovský Token Balík",
"tokensDescriptionText": "Tokeny sa používajú na urýchlenie odomykania\ntruhlíc a na ďalšie funkcie hry a účtu.\n\nŽetóny môžete vyhrať v hre alebo si ich kúpiť\nv balíkoch. Alebo si kúpte Gold Pass na nekonečné\nmnožstvo tokenov a nikdy viac o nich nepočujte.",
"youHaveGoldPassText": "Máte Gold Pass.\nVšetky platby tokenmi sú zadarmo.\nUžívajte!"
},
"topFriendsText": "Top Kamaráti", "topFriendsText": "Top Kamaráti",
"tournamentCheckingStateText": "Kontrolujem štádium turnaju; prosím počkaj...", "tournamentCheckingStateText": "Kontrolujem štádium turnaju; prosím počkaj...",
"tournamentEndedText": "Tento turnaj skončil. Nový začne o chvíľu.", "tournamentEndedText": "Tento turnaj skončil. Nový začne o chvíľu.",
"tournamentEntryText": "Vstupné", "tournamentEntryText": "Vstupné",
"tournamentFinalStandingsText": "Konečné Poradie",
"tournamentResultsRecentText": "Nedávne Výsledky Turnaja", "tournamentResultsRecentText": "Nedávne Výsledky Turnaja",
"tournamentStandingsText": "Postavenie v Turnaji", "tournamentStandingsText": "Postavenie v Turnaji",
"tournamentText": "Turnaj", "tournamentText": "Turnaj",
@ -1476,6 +1527,18 @@
"Uber Onslaught": "Extrémny Útok", "Uber Onslaught": "Extrémny Útok",
"Uber Runaround": "Extrémny Obeh" "Uber Runaround": "Extrémny Obeh"
}, },
"displayItemNames": {
"${C} Tickets": "${C} Tiketov",
"${C} Tokens": "${C} Tokenov",
"Chest": "Truhlica",
"L1 Chest": "Level 1 Truhlica",
"L2 Chest": "Level 2 Truhlica",
"L3 Chest": "Level 3 Truhlica",
"L4 Chest": "Level 4 Truhlica",
"L5 Chest": "Level 5 Truhlica",
"L6 Chest": "Level 6 Truhlica",
"Unknown Chest": "Neznáma Truhlica"
},
"gameDescriptions": { "gameDescriptions": {
"Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Buď vyvolený na určitý čas aby si vyhral.\nZabi vyvoleného sa staň sa vyvoleným.", "Be the chosen one for a length of time to win.\nKill the chosen one to become it.": "Buď vyvolený na určitý čas aby si vyhral.\nZabi vyvoleného sa staň sa vyvoleným.",
"Bomb as many targets as you can.": "Traf čo najviac terčov.", "Bomb as many targets as you can.": "Traf čo najviac terčov.",
@ -1559,7 +1622,8 @@
"Arabic": "Arabčina", "Arabic": "Arabčina",
"Belarussian": "Bieloruština", "Belarussian": "Bieloruština",
"Chinese": "Zjednodušená Čínština", "Chinese": "Zjednodušená Čínština",
"ChineseTraditional": "Tradičná Čínština", "ChineseSimplified": "Čínsky - Zjednodušená",
"ChineseTraditional": "Čínština - Tradičná",
"Croatian": "Chorváčtina", "Croatian": "Chorváčtina",
"Czech": "Čeština", "Czech": "Čeština",
"Danish": "Dánčina", "Danish": "Dánčina",
@ -1580,13 +1644,18 @@
"Korean": "Kórejčina", "Korean": "Kórejčina",
"Malay": "Malajčina", "Malay": "Malajčina",
"Persian": "Perzština", "Persian": "Perzština",
"PirateSpeak": "Pirátske rozprávanie",
"Polish": "Poľština", "Polish": "Poľština",
"Portuguese": "Portugálčina", "Portuguese": "Portugálčina",
"PortugueseBrazil": "Portugalsky - Brazília",
"PortuguesePortugal": "Portugalsky - Portugalsko",
"Romanian": "Rumunčina", "Romanian": "Rumunčina",
"Russian": "Ruština", "Russian": "Ruština",
"Serbian": "Srbčina", "Serbian": "Srbčina",
"Slovak": "Slovenčina", "Slovak": "Slovenčina",
"Spanish": "Španielčina", "Spanish": "Španielčina",
"SpanishLatinAmerica": "Španielsky - Latinská Amerika",
"SpanishSpain": "Španielsky - Španielsko",
"Swedish": "Švédčina", "Swedish": "Švédčina",
"Tamil": "Tamilčina", "Tamil": "Tamilčina",
"Thai": "Thajské", "Thai": "Thajské",
@ -1651,6 +1720,7 @@
"Cheating detected; scores and prizes suspended for ${COUNT} days.": "Zachytili sme podvod; skóre a ceny nedostupné na ${COUNT} dni.", "Cheating detected; scores and prizes suspended for ${COUNT} days.": "Zachytili sme podvod; skóre a ceny nedostupné na ${COUNT} dni.",
"Could not establish a secure connection.": "Nepodarilo sa vytvoriť zabezpečené pripojenie.", "Could not establish a secure connection.": "Nepodarilo sa vytvoriť zabezpečené pripojenie.",
"Daily maximum reached.": "Denný maximum dosiahnutý.", "Daily maximum reached.": "Denný maximum dosiahnutý.",
"Daily sign-in reward": "Denná odmena",
"Entering tournament...": "Vstupujem do turnaja...", "Entering tournament...": "Vstupujem do turnaja...",
"Invalid code.": "Nesprávny kód.", "Invalid code.": "Nesprávny kód.",
"Invalid payment; purchase canceled.": "Neplatná platba; nákup zrušený.", "Invalid payment; purchase canceled.": "Neplatná platba; nákup zrušený.",
@ -1660,11 +1730,14 @@
"Item unlocked!": "Item odomkutý!", "Item unlocked!": "Item odomkutý!",
"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)": "PREPOJENIE RUŠENÉ. ${ACCOUNT} obsahuje\nvzácne dáta ktoré bude STRATENÉ.\nMôžeš prepojiť \"opačne\" ak chceš\n(a stratiť dáta na TOMTO účte).", "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)": "PREPOJENIE RUŠENÉ. ${ACCOUNT} obsahuje\nvzácne dáta ktoré bude STRATENÉ.\nMôžeš prepojiť \"opačne\" ak chceš\n(a stratiť dáta na TOMTO účte).",
"Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "Prepojiť účet ${ACCOUNT} s týmto účtom?\nVšetky dáta na účte ${ACCOUNT} bude stratené.\nToto nemôžeš odvolať. Si si istý?", "Link account ${ACCOUNT} to this account?\nAll existing data on ${ACCOUNT} will be lost.\nThis can not be undone. Are you sure?": "Prepojiť účet ${ACCOUNT} s týmto účtom?\nVšetky dáta na účte ${ACCOUNT} bude stratené.\nToto nemôžeš odvolať. Si si istý?",
"Longer streaks lead to better rewards.": "Dlhšie nepretržitá séria znamená lepšie odmeny.",
"Max number of playlists reached.": "Maximálny počet playlistov dosiahnutý.", "Max number of playlists reached.": "Maximálny počet playlistov dosiahnutý.",
"Max number of profiles reached.": "Maximálny počet profilov dosiahnutý.", "Max number of profiles reached.": "Maximálny počet profilov dosiahnutý.",
"Maximum friend code rewards reached.": "Maximálny počet odmien za nových hráčov dosiahnutý.", "Maximum friend code rewards reached.": "Maximálny počet odmien za nových hráčov dosiahnutý.",
"Message is too long.": "Správa je príliš dlhá.", "Message is too long.": "Správa je príliš dlhá.",
"New tournament result!": "Nový výsledok turnaja.",
"No servers are available. Please try again soon.": "K dispozícii nie sú žiadne servery. Skúste to znova čoskoro.", "No servers are available. Please try again soon.": "K dispozícii nie sú žiadne servery. Skúste to znova čoskoro.",
"No slots available. Free a slot and try again.": "Žiadny slot voľný. Uvoľnite slot a skúste znova.",
"Profile \"${NAME}\" upgraded successfully.": "Profil \"${NAME}\" bol úspešne vylepšený.", "Profile \"${NAME}\" upgraded successfully.": "Profil \"${NAME}\" bol úspešne vylepšený.",
"Profile could not be upgraded.": "Profil nemožno vylepšiť.", "Profile could not be upgraded.": "Profil nemožno vylepšiť.",
"Purchase successful!": "Nákup prebehol úspešne!", "Purchase successful!": "Nákup prebehol úspešne!",
@ -1674,7 +1747,9 @@
"Sorry, this code has already been used.": "Prepáč, tento kód už bol použitý.", "Sorry, this code has already been used.": "Prepáč, tento kód už bol použitý.",
"Sorry, this code has expired.": "Prepáč, platnosť kódu vypršala.", "Sorry, this code has expired.": "Prepáč, platnosť kódu vypršala.",
"Sorry, this code only works for new accounts.": "Prepáč, tento kód funguje len pre nové účty.", "Sorry, this code only works for new accounts.": "Prepáč, tento kód funguje len pre nové účty.",
"Sorry, this has expired.": "Pardón, toto vypršalo.",
"Still searching for nearby servers; please try again soon.": "Still searching for nearby servers; please try again soon.", "Still searching for nearby servers; please try again soon.": "Still searching for nearby servers; please try again soon.",
"Streak: ${NUM} days": "Séria: ${NUM} dní",
"Temporarily unavailable; please try again later.": "Dočasne nedostupné; prosím skús to znova neskôr.", "Temporarily unavailable; please try again later.": "Dočasne nedostupné; prosím skús to znova neskôr.",
"The tournament ended before you finished.": "Turnaj sa skončil predtým ako si ho dokončil.", "The tournament ended before you finished.": "Turnaj sa skončil predtým ako si ho dokončil.",
"This account cannot be unlinked for ${NUM} days.": "Tento účet nemôže byť odpojený v podobu ${NUM} dní.", "This account cannot be unlinked for ${NUM} days.": "Tento účet nemôže byť odpojený v podobu ${NUM} dní.",
@ -1685,19 +1760,28 @@
"Tournaments require ${VERSION} or newer": "Turnaje vyžadujú ${VERSION} alebo novšiu.", "Tournaments require ${VERSION} or newer": "Turnaje vyžadujú ${VERSION} alebo novšiu.",
"Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "Odpojiť ${ACCOUNT} z tohoto zariadenia?\nVšetky dáta na ${ACCOUNT} sa resetujú.\n(okrem achievementov pri niektorých prípadoch)", "Unlink ${ACCOUNT} from this account?\nAll data on ${ACCOUNT} will be reset.\n(except for achievements in some cases)": "Odpojiť ${ACCOUNT} z tohoto zariadenia?\nVšetky dáta na ${ACCOUNT} sa resetujú.\n(okrem achievementov pri niektorých prípadoch)",
"WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "Varovanie: našli sa náznaky podvádzania na tomto účte.\nÚčty ktoré podvádzajú budú zabanované. Prosím hraj férovo.", "WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "Varovanie: našli sa náznaky podvádzania na tomto účte.\nÚčty ktoré podvádzajú budú zabanované. Prosím hraj férovo.",
"Wait reduced!": "Čakanie Skrátené!",
"Warning: This version of the game is limited to old account data; things may appear missing or out of date.\nPlease upgrade to a newer version of the game to see your latest account data.": "Upozornenie: Táto verzia hry je obmedzená na staré údaje o účte; niektoré položky môžu chýbať alebo byť neaktuálne.\nProsím nainštalujte novšiu verziu hry pre videnie najnovších údajov o vašom účte.",
"Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "Chcel by si prepojiť tvoj účet zariadenia na tento účet?\n\nTvoj účet zariadenia je {ACCOUNT1}\nTento účet je ${ACCOUNT2}\n\nToto ti povolí ponechať tvoje pokroky.\nVarovanie: toto nejde vrátiť!", "Would you like to link your device account to this one?\n\nYour device account is ${ACCOUNT1}\nThis account is ${ACCOUNT2}\n\nThis will allow you to keep your existing progress.\nWarning: this cannot be undone!\n": "Chcel by si prepojiť tvoj účet zariadenia na tento účet?\n\nTvoj účet zariadenia je {ACCOUNT1}\nTento účet je ${ACCOUNT2}\n\nToto ti povolí ponechať tvoje pokroky.\nVarovanie: toto nejde vrátiť!",
"You already own this!": "Toto už vlastníš!", "You already own this!": "Toto už vlastníš!",
"You can join in ${COUNT} seconds.": "Môžeš sa pripojiť za ${COUNT} sekúnd.", "You can join in ${COUNT} seconds.": "Môžeš sa pripojiť za ${COUNT} sekúnd.",
"You don't have enough tickets for this!": "Na toto nemáš tikety!", "You don't have enough tickets for this!": "Na toto nemáš tikety!",
"You don't own that.": "Toto ešte nevlastníš.", "You don't own that.": "Toto ešte nevlastníš.",
"You got ${COUNT} tickets!": "Máš ${COUNT} tiketov!", "You got ${COUNT} tickets!": "Máš ${COUNT} tiketov!",
"You got ${COUNT} tokens!": "Máš ${COUNT} tokenov!",
"You got a ${ITEM}!": "Máš ${ITEM}!", "You got a ${ITEM}!": "Máš ${ITEM}!",
"You got a chest!": "Maš truhlicu!",
"You got an achievement reward!": "Máš odmenu za úspech!",
"You have been promoted to a new league; congratulations!": "Bol si povýšený na vyššiu ligu; gratulujeme!", "You have been promoted to a new league; congratulations!": "Bol si povýšený na vyššiu ligu; gratulujeme!",
"You lost a chest! (All your chest slots were full)": "Stratil si truhlicu! (Všetky sloty na truhlice boli plné)",
"You must update the app to view this.": "Musíš nainštalovať aktuálnu verziu aby si to videl.",
"You must update to a newer version of the app to do this.": "Musíš nainštalovať novšiu verziu hry ak chceš toto spraviť.", "You must update to a newer version of the app to do this.": "Musíš nainštalovať novšiu verziu hry ak chceš toto spraviť.",
"You must update to the newest version of the game to do this.": "Musíš nainštalovať najnovšiu verziu hry ak chceš toto spraviť.", "You must update to the newest version of the game to do this.": "Musíš nainštalovať najnovšiu verziu hry ak chceš toto spraviť.",
"You must wait a few seconds before entering a new code.": "Musíš počkať pár sekúnd predtým ako vložíš nový kód.", "You must wait a few seconds before entering a new code.": "Musíš počkať pár sekúnd predtým ako vložíš nový kód.",
"You placed #${RANK} in a tournament!": "Skončil si #${RANK} v turnaji!",
"You ranked #${RANK} in the last tournament. Thanks for playing!": "Umiestnil si sa na #${RANK} mieste v turnaji. Vďaka za hranie!", "You ranked #${RANK} in the last tournament. Thanks for playing!": "Umiestnil si sa na #${RANK} mieste v turnaji. Vďaka za hranie!",
"Your account was rejected. Are you signed in?": "Tvoj účet nebol prijatý. Si prihlásený?", "Your account was rejected. Are you signed in?": "Tvoj účet nebol prijatý. Si prihlásený?",
"Your ad views are not registering. Ad options will be limited for a while.": "Vaše zobrazenia reklám sa nezaznamenávajú. Možnosti reklám budú na chvíľu limitované.",
"Your copy of the game has been modified.\nPlease revert any changes and try again.": "Tvoja kópia hry je modifikovaná.\nProsím vráť späť všetky zmeny a skús to znova.", "Your copy of the game has been modified.\nPlease revert any changes and try again.": "Tvoja kópia hry je modifikovaná.\nProsím vráť späť všetky zmeny a skús to znova.",
"Your friend code was used by ${ACCOUNT}": "Tvoj kód bol použitý hráčom ${ACCOUNT}" "Your friend code was used by ${ACCOUNT}": "Tvoj kód bol použitý hráčom ${ACCOUNT}"
}, },
@ -1846,7 +1930,9 @@
"toSkipPressAnythingText": "(stlač dačo pre preskočenie tutoriálu)" "toSkipPressAnythingText": "(stlač dačo pre preskočenie tutoriálu)"
}, },
"twoKillText": "DOUBLE-KILL!", "twoKillText": "DOUBLE-KILL!",
"uiScaleText": "Veľkosť UI",
"unavailableText": "nedostupné", "unavailableText": "nedostupné",
"unclaimedPrizesText": "Máš nevyzdvihnuté darčeky!",
"unconfiguredControllerDetectedText": "Nenastavený ovládač detekovaný:", "unconfiguredControllerDetectedText": "Nenastavený ovládač detekovaný:",
"unlockThisInTheStoreText": "Toto musí byť odomknuté v obchode.", "unlockThisInTheStoreText": "Toto musí byť odomknuté v obchode.",
"unlockThisProfilesText": "Ak chceš vytvoriť viac ako ${NUM} profilov, potrebuješ:", "unlockThisProfilesText": "Ak chceš vytvoriť viac ako ${NUM} profilov, potrebuješ:",
@ -1859,9 +1945,12 @@
"upgradeText": "Vylepšiť", "upgradeText": "Vylepšiť",
"upgradeToPlayText": "Odomkni \"${PRO}\" v obchode (v hre) ak toto chceš hrať.", "upgradeToPlayText": "Odomkni \"${PRO}\" v obchode (v hre) ak toto chceš hrať.",
"useDefaultText": "Použiť Štandartné", "useDefaultText": "Použiť Štandartné",
"userSystemScriptsCreateText": "Vytvoriť Užívateľské Systémové Scripty",
"userSystemScriptsDeleteText": "Vymazať Užívateľské Systémové Scripty",
"usesExternalControllerText": "Táto hra používa ako vstup externý ovládač.", "usesExternalControllerText": "Táto hra používa ako vstup externý ovládač.",
"usingItunesText": "Používam Music App ako soundtrack...", "usingItunesText": "Používam Music App ako soundtrack...",
"v2AccountLinkingInfoText": "Na prepojenie V2 účtov, použi tlačidlo 'Spravovať Účet'.", "v2AccountLinkingInfoText": "Na prepojenie V2 účtov, použi tlačidlo 'Spravovať Účet'.",
"v2AccountRequiredText": "Toto vyžaduje V2 účet. Aktualizuje svoj účet a skúste znova.",
"validatingTestBuildText": "Overujem Testovaciu Verziu...", "validatingTestBuildText": "Overujem Testovaciu Verziu...",
"viaText": "ako", "viaText": "ako",
"victoryText": "Výhra!", "victoryText": "Výhra!",
@ -1928,5 +2017,6 @@
}, },
"yesAllowText": "Áno, Povoliť!", "yesAllowText": "Áno, Povoliť!",
"yourBestScoresText": "Tvoje Najlepšie Skóre", "yourBestScoresText": "Tvoje Najlepšie Skóre",
"yourBestTimesText": "Tvoje Najlepšie Časy" "yourBestTimesText": "Tvoje Najlepšie Časy",
"yourPrizeText": "Tvoja odmena:"
} }

View file

@ -1,27 +1,27 @@
{ {
"accountSettingsWindow": { "accountSettingsWindow": {
"accountNameRules": "Los nombres de las cuentas no pueden tener emojis o otros caracteres especiales", "accountNameRules": "El Tag d La cuenta no puede contener emojis ni caracteres especiales",
"accountsText": "Cuentas", "accountsText": "Cuentas",
"achievementProgressText": "Logros: ${COUNT} de ${TOTAL}", "achievementProgressText": "Logros: ${COUNT} de ${TOTAL}",
"campaignProgressText": "Progreso de la Campaña [Difícil]: ${PROGRESS}", "campaignProgressText": "Progreso de la Campaña [Difícil]: ${PROGRESS}",
"changeOncePerSeason": "Solo puedes cambiar esto una vez por temporada.", "changeOncePerSeason": "Solo puedes cambiar esto una vez por temporada.",
"changeOncePerSeasonError": "Debes esperar hasta la siguiente temporada para cambiar esto de nuevo (en ${NUM} día/s)", "changeOncePerSeasonError": "Debes esperar hasta la siguiente temporada para cambiar esto otra vez (${NUM} día/s)",
"createAnAccountText": "Crear una Cuenta", "createAnAccountText": "Crea una Cuenta",
"customName": "Nombre Personalizado", "customName": "Nombre Personalizado",
"deleteAccountText": "Eliminar Cuenta", "deleteAccountText": "Borrar Cuenta",
"googlePlayGamesAccountSwitchText": "Si quieres usar una cuenta de Google diferente,\nusa la app Google Play Juegos para cambiarla.", "googlePlayGamesAccountSwitchText": "Si Queres Usar una Cuenta diferente, usa la d Google Play Juegos para cambiarla.\n(NOTA: Ya No c si funciona)",
"linkAccountsEnterCodeText": "Introducir Código", "linkAccountsEnterCodeText": "Ingresar Código",
"linkAccountsGenerateCodeText": "Generar Código", "linkAccountsGenerateCodeText": "Generar Código",
"linkAccountsInfoText": "(comparta el progreso a través de diferentes plataformas)", "linkAccountsInfoText": "(comparte tu progreso con diferentes plataformas)",
"linkAccountsInstructionsNewText": "Para vincular dos cuentas, genera un código en la primera \ne introduzca ese código en la segunda. Los datos de \nla segunda cuenta serán compartidos entre ambos.\n(Los datos de la primera cuenta se perderán).\n\nPuedes vincular hasta ${COUNT} cuentas.\n\nIMPORTANTE: Solo vincula cuentas tuyas; \nSi vinculas cuentas de tus amigos no serán \ncapaces de jugar en línea al mismo tiempo.", "linkAccountsInstructionsNewText": "Para vincular dos cuentas, genera un código en la primera e ingresa ese código en la segunda.\nDatos de la segunda cuenta serán compartidas entre las dos.\n(Datos de la primera cuenta se perderan)\n\n\nPuedes vincular hasta ${COUNT} cuentas.\n\nIMPORTANTE: Solo vincula cuentas tuyas; \nSi vinculas cuentas de tus amigos no serán \ncapaces de jugar en línea al mismo tiempo.",
"linkAccountsText": "Vincular Cuentas", "linkAccountsText": "Vincular Cuentas",
"linkedAccountsText": "Cuentas Vinculadas:", "linkedAccountsText": "Cuentas Vinculadas:",
"manageAccountText": "Administrar Cuenta", "manageAccountText": "Administrar Cuenta",
"nameChangeConfirm": "¿Quieres cambiar El nombre de tu cuenta A ${NAME}?", "nameChangeConfirm": "¿Cambiar el nombre de tu cuenta a ${NAME}?",
"resetProgressConfirmNoAchievementsText": "Esto reiniciará tu progreso en el modo cooperativo y \ntus puntuaciones locales (a excepción de tus boletos).\nEsto no puede deshacerse. ¿Estás seguro?", "resetProgressConfirmNoAchievementsText": "Esto va a reiniciar tu progreso en el modo cooperativo y \ntus puntuaciones locales (excepto tus boletos).\nEsto no se puede deshacer. ¿Estás seguro?",
"resetProgressConfirmText": "Esto reiniciará tu progreso en el modo cooperativo, \nlogros y puntuaciones locales (pero, no tus tickets).\nLos cambios no se pueden deshacer.\n¿Estás seguro?", "resetProgressConfirmText": "Esto va a reiniciar tu progreso en el modo cooperativo, tus\nlogros y tus puntuaciones locales (excepto tus tickets).\nEstos cambios no se pueden deshacer.\n¿Estás seguro?",
"resetProgressText": "Reiniciar Progreso", "resetProgressText": "Reiniciar Progreso",
"setAccountName": "Establecer Nombre de Cuenta", "setAccountName": "Ingresar Nombre de Cuenta",
"setAccountNameDesc": "Selecciona el nombre a mostrar para tu cuenta. \nPuedes usar el nombre de tu cuenta asociada\no crear un nombre único personalizado.", "setAccountNameDesc": "Selecciona el nombre a mostrar para tu cuenta. \nPuedes usar el nombre de tu cuenta asociada\no crear un nombre único personalizado.",
"signInInfoText": "Inicia sesión para obtener boletos, competir en línea\ny compartir tu progreso a través de tus dispositivos.", "signInInfoText": "Inicia sesión para obtener boletos, competir en línea\ny compartir tu progreso a través de tus dispositivos.",
"signInText": "Iniciar Sesión", "signInText": "Iniciar Sesión",
@ -1576,7 +1576,7 @@
}, },
"gameNames": { "gameNames": {
"Assault": "Asalto", "Assault": "Asalto",
"Capture the Flag": "Captura la Bandera", "Capture the Flag": "Captura De Banderas",
"Chosen One": "El Elegido", "Chosen One": "El Elegido",
"Conquest": "Conquista", "Conquest": "Conquista",
"Death Match": "Combate Mortal", "Death Match": "Combate Mortal",
@ -1621,6 +1621,7 @@
"Indonesian": "Indonesio", "Indonesian": "Indonesio",
"Italian": "Italiano", "Italian": "Italiano",
"Japanese": "Japonés", "Japanese": "Japonés",
"Kazakh": "Kazajo",
"Korean": "Coreano", "Korean": "Coreano",
"Malay": "Malayo", "Malay": "Malayo",
"Persian": "Persa", "Persian": "Persa",
@ -1754,6 +1755,7 @@
"You got an achievement reward!": "¡Obtuviste una recompensa por tu logro!", "You got an achievement reward!": "¡Obtuviste una recompensa por tu logro!",
"You have been promoted to a new league; congratulations!": "Has sido ascendido a una nueva liga; ¡felicitaciones!", "You have been promoted to a new league; congratulations!": "Has sido ascendido a una nueva liga; ¡felicitaciones!",
"You lost a chest! (All your chest slots were full)": "¡Perdiste un cofre! (Todas tus ranuras de cofres estaban llenas).", "You lost a chest! (All your chest slots were full)": "¡Perdiste un cofre! (Todas tus ranuras de cofres estaban llenas).",
"You must sign in to do this.": "Debes iniciar sesión para hacer esto.",
"You must update the app to view this.": "Debes actualizar la aplicación para ver esto.", "You must update the app to view this.": "Debes actualizar la aplicación para ver esto.",
"You must update to a newer version of the app to do this.": "Debes actualizar la aplicación a una versión más reciente para hacer esto.", "You must update to a newer version of the app to do this.": "Debes actualizar la aplicación a una versión más reciente para hacer esto.",
"You must update to the newest version of the game to do this.": "Necesitas actualizar a la versión más reciente del juego para hacer esto.", "You must update to the newest version of the game to do this.": "Necesitas actualizar a la versión más reciente del juego para hacer esto.",
@ -1786,7 +1788,7 @@
"Entire Team Must Finish": "Todo el Equipo Debe Terminar", "Entire Team Must Finish": "Todo el Equipo Debe Terminar",
"Epic Mode": "Modo Épico", "Epic Mode": "Modo Épico",
"Flag Idle Return Time": "Tiempo de Retorno de Bandera Inactiva", "Flag Idle Return Time": "Tiempo de Retorno de Bandera Inactiva",
"Flag Touch Return Time": "Retorno de Bandera con Toque", "Flag Touch Return Time": "Retorno de Bandera al Tocarla",
"Hold Time": "Retención", "Hold Time": "Retención",
"Kills to Win Per Player": "Asesinatos para Ganar Por Jugador", "Kills to Win Per Player": "Asesinatos para Ganar Por Jugador",
"Laps": "Vueltas", "Laps": "Vueltas",

View file

@ -1621,6 +1621,7 @@
"Indonesian": "Indonesio", "Indonesian": "Indonesio",
"Italian": "Italiano", "Italian": "Italiano",
"Japanese": "Japonés", "Japanese": "Japonés",
"Kazakh": "Kazajo",
"Korean": "Coreano", "Korean": "Coreano",
"Malay": "Malayo", "Malay": "Malayo",
"Persian": "Persa", "Persian": "Persa",

View file

@ -7,6 +7,7 @@
"campaignProgressText": "Kampanjutveckling: [svår] ${PROGRESS}", "campaignProgressText": "Kampanjutveckling: [svår] ${PROGRESS}",
"changeOncePerSeason": "Du kan enbart ändra detta en gång per säsong.", "changeOncePerSeason": "Du kan enbart ändra detta en gång per säsong.",
"changeOncePerSeasonError": "Du måste vänta tills nästa säsong för att ändra detta igen (${NUM} days)", "changeOncePerSeasonError": "Du måste vänta tills nästa säsong för att ändra detta igen (${NUM} days)",
"createAnAccountText": "Skapa ett konto",
"customName": "Anpassat Namn", "customName": "Anpassat Namn",
"deleteAccountText": "Ta bort konto", "deleteAccountText": "Ta bort konto",
"googlePlayGamesAccountSwitchText": "Om du vill använda ett annat Google-konto,\nanvänd appen Google Play Spel för att byta.", "googlePlayGamesAccountSwitchText": "Om du vill använda ett annat Google-konto,\nanvänd appen Google Play Spel för att byta.",

View file

@ -1620,7 +1620,8 @@
"Arabic": "Arapça", "Arabic": "Arapça",
"Belarussian": "Beyazrusça", "Belarussian": "Beyazrusça",
"Chinese": "Basitleştirilmiş Çince", "Chinese": "Basitleştirilmiş Çince",
"ChineseTraditional": "Geleneksel Çince", "ChineseSimplified": "Çince basitleştirilmiş",
"ChineseTraditional": "Geleneksel çince",
"Croatian": "Hırvatça", "Croatian": "Hırvatça",
"Czech": "Çekçe", "Czech": "Çekçe",
"Danish": "Danimarkaca", "Danish": "Danimarkaca",
@ -1638,17 +1639,22 @@
"Indonesian": "Endonezyaca", "Indonesian": "Endonezyaca",
"Italian": "İtalyanca", "Italian": "İtalyanca",
"Japanese": "Japonca", "Japanese": "Japonca",
"Kazakh": "Kazakça",
"Korean": "Korece", "Korean": "Korece",
"Malay": "Malayca", "Malay": "Malayca",
"Persian": "Farsça", "Persian": "Farsça",
"PirateSpeak": "Korsan Dili", "PirateSpeak": "Korsan Dili",
"Polish": "Polonya Dili", "Polish": "Polonya Dili",
"Portuguese": "Portekizce", "Portuguese": "Portekizce",
"PortugueseBrazil": "Portekizce (brezilya)",
"PortuguesePortugal": "Portekizce (portekiz)",
"Romanian": "Romence", "Romanian": "Romence",
"Russian": "Rusça", "Russian": "Rusça",
"Serbian": "Sırpça", "Serbian": "Sırpça",
"Slovak": "Slovakça", "Slovak": "Slovakça",
"Spanish": "İspanyolca", "Spanish": "İspanyolca",
"SpanishLatinAmerica": "İspanyolca (latin amerika)",
"SpanishSpain": "İspanyolca (ispanya)",
"Swedish": "İsveççe", "Swedish": "İsveççe",
"Tamil": "Tamilce", "Tamil": "Tamilce",
"Thai": "Tayland dili", "Thai": "Tayland dili",

View file

@ -1620,7 +1620,8 @@
"Arabic": "Арабська", "Arabic": "Арабська",
"Belarussian": "Білоруська", "Belarussian": "Білоруська",
"Chinese": "Китайська", "Chinese": "Китайська",
"ChineseTraditional": "Китайська традиційна", "ChineseSimplified": "Китайська - Спрощена",
"ChineseTraditional": "Китайська - Традиційна",
"Croatian": "Хорватська", "Croatian": "Хорватська",
"Czech": "Чеська", "Czech": "Чеська",
"Danish": "Данська", "Danish": "Данська",
@ -1638,17 +1639,22 @@
"Indonesian": "Індонезійська", "Indonesian": "Індонезійська",
"Italian": "Італійська", "Italian": "Італійська",
"Japanese": "Японська", "Japanese": "Японська",
"Kazakh": "Казахська",
"Korean": "Корейська", "Korean": "Корейська",
"Malay": "Малайська", "Malay": "Малайська",
"Persian": "Перська", "Persian": "Перська",
"PirateSpeak": "Піратська мова", "PirateSpeak": "Піратська мова",
"Polish": "Польська", "Polish": "Польська",
"Portuguese": "Португальська", "Portuguese": "Португальська",
"PortugueseBrazil": "Португальська - Бразилія",
"PortuguesePortugal": "Португальська - Португалія",
"Romanian": "Румунська", "Romanian": "Румунська",
"Russian": "Російська", "Russian": "Російська",
"Serbian": "Сербська", "Serbian": "Сербська",
"Slovak": "Словацька", "Slovak": "Словацька",
"Spanish": "Іспанська", "Spanish": "Іспанська",
"SpanishLatinAmerica": "Іспанська - Латинська Америка",
"SpanishSpain": "Іспанська - Іспанія",
"Swedish": "Шведська", "Swedish": "Шведська",
"Tamil": "тамільська", "Tamil": "тамільська",
"Thai": "Тайська", "Thai": "Тайська",
@ -1767,6 +1773,7 @@
"You got an achievement reward!": "Ви отримали нагороду за досягнення!", "You got an achievement reward!": "Ви отримали нагороду за досягнення!",
"You have been promoted to a new league; congratulations!": "Вас підвищили і перевели в нову лігу; вітаємо!", "You have been promoted to a new league; congratulations!": "Вас підвищили і перевели в нову лігу; вітаємо!",
"You lost a chest! (All your chest slots were full)": "Ви втратили скриню! (Всі ваші слоти для скринь заповненні)", "You lost a chest! (All your chest slots were full)": "Ви втратили скриню! (Всі ваші слоти для скринь заповненні)",
"You must sign in to do this.": "Ви повинні увійти, щоб зробити це.",
"You must update the app to view this.": "Щоб переглянути це, потрібно оновити програму.", "You must update the app to view this.": "Щоб переглянути це, потрібно оновити програму.",
"You must update to a newer version of the app to do this.": "Щоб це зробити, ви повинні оновити додаток.", "You must update to a newer version of the app to do this.": "Щоб це зробити, ви повинні оновити додаток.",
"You must update to the newest version of the game to do this.": "Ви повинні оновитися до нової версії гри, щоб зробити це.", "You must update to the newest version of the game to do this.": "Ви повинні оновитися до нової версії гри, щоб зробити це.",

View file

@ -1622,7 +1622,8 @@
"Arabic": "Tiếng Ả Rập", "Arabic": "Tiếng Ả Rập",
"Belarussian": "Tiếng Belarus", "Belarussian": "Tiếng Belarus",
"Chinese": "Tiếng Trung Giản thể", "Chinese": "Tiếng Trung Giản thể",
"ChineseTraditional": "Tiếng Trung Quốc truyền thống", "ChineseSimplified": "Tiếng Trung - Giản Thể",
"ChineseTraditional": "Tiếng Trung - Quốc truyền thống",
"Croatian": "Tiếng Croatia", "Croatian": "Tiếng Croatia",
"Czech": "Tiếng Séc", "Czech": "Tiếng Séc",
"Danish": "Tiếng Đan Mạch", "Danish": "Tiếng Đan Mạch",
@ -1646,11 +1647,15 @@
"PirateSpeak": "Tiếng Hải tặc", "PirateSpeak": "Tiếng Hải tặc",
"Polish": "Tiếng Polish", "Polish": "Tiếng Polish",
"Portuguese": "Tiếng Bồ Đào Nha", "Portuguese": "Tiếng Bồ Đào Nha",
"PortugueseBrazil": "Bồ Đào Nha - Brazil",
"PortuguesePortugal": "Tiếng Bồ Đào Nha - Bồ Đào Nha",
"Romanian": "Tiếng Rumani", "Romanian": "Tiếng Rumani",
"Russian": "Tiếng Nga", "Russian": "Tiếng Nga",
"Serbian": "Tiếng Serbia", "Serbian": "Tiếng Serbia",
"Slovak": "Tiếng Slovakia", "Slovak": "Tiếng Slovakia",
"Spanish": "Tiếng Tây Ban Nha", "Spanish": "Tiếng Tây Ban Nha",
"SpanishLatinAmerica": "Tây Ban Nha - Mỹ Latinh",
"SpanishSpain": "Tây Ban Nha - Tây Ban Nha",
"Swedish": "Tiếng Thụy Điển", "Swedish": "Tiếng Thụy Điển",
"Tamil": "Tamil", "Tamil": "Tamil",
"Thai": "Tiếng thái", "Thai": "Tiếng thái",

View file

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

View file

@ -1,95 +1,4 @@
# Issuer: CN=GlobalSign Root CA O=GlobalSign nv-sa OU=Root CA
# Subject: CN=GlobalSign Root CA O=GlobalSign nv-sa OU=Root CA
# Label: "GlobalSign Root CA"
# Serial: 4835703278459707669005204
# MD5 Fingerprint: 3e:45:52:15:09:51:92:e1:b7:5d:37:9f:b1:87:29:8a
# SHA1 Fingerprint: b1:bc:96:8b:d4:f4:9d:62:2a:a8:9a:81:f2:15:01:52:a4:1d:82:9c
# SHA256 Fingerprint: eb:d4:10:40:e4:bb:3e:c7:42:c9:e3:81:d3:1e:f2:a4:1a:48:b6:68:5c:96:e7:ce:f3:c1:df:6c:d4:33:1c:99
-----BEGIN CERTIFICATE-----
MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG
A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv
b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw
MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i
YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT
aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ
jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp
xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp
1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG
snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ
U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8
9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E
BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B
AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz
yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE
38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP
AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad
DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME
HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A==
-----END CERTIFICATE-----
# Issuer: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited
# Subject: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited
# Label: "Entrust.net Premium 2048 Secure Server CA"
# Serial: 946069240
# MD5 Fingerprint: ee:29:31:bc:32:7e:9a:e6:e8:b5:f7:51:b4:34:71:90
# SHA1 Fingerprint: 50:30:06:09:1d:97:d4:f5:ae:39:f7:cb:e7:92:7d:7d:65:2d:34:31
# SHA256 Fingerprint: 6d:c4:71:72:e0:1c:bc:b0:bf:62:58:0d:89:5f:e2:b8:ac:9a:d4:f8:73:80:1e:0c:10:b9:c8:37:d2:1e:b1:77
-----BEGIN CERTIFICATE-----
MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChML
RW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBp
bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5
IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENlcnRp
ZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQxNzUwNTFaFw0yOTA3
MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3
LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp
YWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG
A1UEAxMqRW50cnVzdC5uZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgp
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArU1LqRKGsuqjIAcVFmQq
K0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOLGp18EzoOH1u3Hs/lJBQe
sYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSrhRSGlVuX
MlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVT
XTzWnLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/
HoZdenoVve8AjhUiVBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH
4QIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV
HQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJKoZIhvcNAQEFBQADggEBADub
j1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPyT/4xmf3IDExo
U8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf
zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5b
u/8j72gZyxKTJ1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+
bYQLCIt+jerXmCHG8+c8eS9enNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/Er
fF6adulZkMV8gzURZVE=
-----END CERTIFICATE-----
# Issuer: CN=Baltimore CyberTrust Root O=Baltimore OU=CyberTrust
# Subject: CN=Baltimore CyberTrust Root O=Baltimore OU=CyberTrust
# Label: "Baltimore CyberTrust Root"
# Serial: 33554617
# MD5 Fingerprint: ac:b6:94:a5:9c:17:e0:d7:91:52:9b:b1:97:06:a6:e4
# SHA1 Fingerprint: d4:de:20:d0:5e:66:fc:53:fe:1a:50:88:2c:78:db:28:52:ca:e4:74
# SHA256 Fingerprint: 16:af:57:a9:f6:76:b0:ab:12:60:95:aa:5e:ba:de:f2:2a:b3:11:19:d6:44:ac:95:cd:4b:93:db:f3:f2:6a:eb
-----BEGIN CERTIFICATE-----
MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ
RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD
VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX
DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y
ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy
VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr
mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr
IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK
mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu
XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy
dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye
jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1
BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3
DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92
9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx
jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0
Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz
ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS
R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp
-----END CERTIFICATE-----
# Issuer: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc. # Issuer: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc.
# Subject: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc. # Subject: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc.
# Label: "Entrust Root Certification Authority" # Label: "Entrust Root Certification Authority"
@ -125,39 +34,6 @@ eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m
0vdXcDazv/wor3ElhVsT/h5/WrQ8 0vdXcDazv/wor3ElhVsT/h5/WrQ8
-----END CERTIFICATE----- -----END CERTIFICATE-----
# Issuer: CN=AAA Certificate Services O=Comodo CA Limited
# Subject: CN=AAA Certificate Services O=Comodo CA Limited
# Label: "Comodo AAA Services root"
# Serial: 1
# MD5 Fingerprint: 49:79:04:b0:eb:87:19:ac:47:b0:bc:11:51:9b:74:d0
# SHA1 Fingerprint: d1:eb:23:a4:6d:17:d6:8f:d9:25:64:c2:f1:f1:60:17:64:d8:e3:49
# SHA256 Fingerprint: d7:a7:a0:fb:5d:7e:27:31:d7:71:e9:48:4e:bc:de:f7:1d:5f:0c:3e:0a:29:48:78:2b:c8:3e:e0:ea:69:9e:f4
-----BEGIN CERTIFICATE-----
MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb
MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow
GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj
YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL
MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE
BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM
GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP
ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua
BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe
3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4
YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR
rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm
ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU
oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF
MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v
QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t
b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF
AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q
GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz
Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2
G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi
l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3
smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg==
-----END CERTIFICATE-----
# Issuer: CN=QuoVadis Root CA 2 O=QuoVadis Limited # Issuer: CN=QuoVadis Root CA 2 O=QuoVadis Limited
# Subject: CN=QuoVadis Root CA 2 O=QuoVadis Limited # Subject: CN=QuoVadis Root CA 2 O=QuoVadis Limited
# Label: "QuoVadis Root CA 2" # Label: "QuoVadis Root CA 2"
@ -245,103 +121,6 @@ mJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK
4SVhM7JZG+Ju1zdXtg2pEto= 4SVhM7JZG+Ju1zdXtg2pEto=
-----END CERTIFICATE----- -----END CERTIFICATE-----
# Issuer: CN=XRamp Global Certification Authority O=XRamp Security Services Inc OU=www.xrampsecurity.com
# Subject: CN=XRamp Global Certification Authority O=XRamp Security Services Inc OU=www.xrampsecurity.com
# Label: "XRamp Global CA Root"
# Serial: 107108908803651509692980124233745014957
# MD5 Fingerprint: a1:0b:44:b3:ca:10:d8:00:6e:9d:0f:d8:0f:92:0a:d1
# SHA1 Fingerprint: b8:01:86:d1:eb:9c:86:a5:41:04:cf:30:54:f3:4c:52:b7:e5:58:c6
# SHA256 Fingerprint: ce:cd:dc:90:50:99:d8:da:df:c5:b1:d2:09:b7:37:cb:e2:c1:8c:fb:2c:10:c0:ff:0b:cf:0d:32:86:fc:1a:a2
-----BEGIN CERTIFICATE-----
MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCB
gjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEk
MCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRY
UmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQxMTAxMTcx
NDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3
dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2Vy
dmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB
dXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS6
38eMpSe2OAtp87ZOqCwuIR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCP
KZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMxfoArtYzAQDsRhtDLooY2YKTVMIJt2W7Q
DxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FEzG+gSqmUsE3a56k0enI4
qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqsAxcZZPRa
JSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNVi
PvryxS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0P
BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASs
jVy16bYbMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0
eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQEwDQYJKoZIhvcNAQEFBQAD
ggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc/Kh4ZzXxHfAR
vbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt
qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLa
IR9NmXmd4c8nnxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSy
i6mx5O+aGtA9aZnuqCij4Tyz8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQ
O+7ETPTsJ3xCwnR8gooJybQDJbw=
-----END CERTIFICATE-----
# Issuer: O=The Go Daddy Group, Inc. OU=Go Daddy Class 2 Certification Authority
# Subject: O=The Go Daddy Group, Inc. OU=Go Daddy Class 2 Certification Authority
# Label: "Go Daddy Class 2 CA"
# Serial: 0
# MD5 Fingerprint: 91:de:06:25:ab:da:fd:32:17:0c:bb:25:17:2a:84:67
# SHA1 Fingerprint: 27:96:ba:e6:3f:18:01:e2:77:26:1b:a0:d7:77:70:02:8f:20:ee:e4
# SHA256 Fingerprint: c3:84:6b:f2:4b:9e:93:ca:64:27:4c:0e:c6:7c:1e:cc:5e:02:4f:fc:ac:d2:d7:40:19:35:0e:81:fe:54:6a:e4
-----BEGIN CERTIFICATE-----
MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh
MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE
YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3
MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo
ZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg
MiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN
ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA
PVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w
wdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi
EqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY
avx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+
YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE
sNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h
/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5
IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj
YXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD
ggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy
OO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P
TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ
HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER
dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf
ReYNnyicsbkqWletNw+vHX/bvZ8=
-----END CERTIFICATE-----
# Issuer: O=Starfield Technologies, Inc. OU=Starfield Class 2 Certification Authority
# Subject: O=Starfield Technologies, Inc. OU=Starfield Class 2 Certification Authority
# Label: "Starfield Class 2 CA"
# Serial: 0
# MD5 Fingerprint: 32:4a:4b:bb:c8:63:69:9b:be:74:9a:c6:dd:1d:46:24
# SHA1 Fingerprint: ad:7e:1c:28:b0:64:ef:8f:60:03:40:20:14:c3:d0:e3:37:0e:b5:8a
# SHA256 Fingerprint: 14:65:fa:20:53:97:b8:76:fa:a6:f0:a9:95:8e:55:90:e4:0f:cc:7f:aa:4f:b7:c2:c8:67:75:21:fb:5f:b6:58
-----BEGIN CERTIFICATE-----
MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl
MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp
U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw
NjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE
ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp
ZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3
DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf
8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN
+lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0
X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa
K4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA
1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G
A1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR
zt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0
YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD
bGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w
DQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3
L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D
eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl
xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp
VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY
WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q=
-----END CERTIFICATE-----
# Issuer: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com # Issuer: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com
# Subject: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com # Subject: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com
# Label: "DigiCert Assured ID Root CA" # Label: "DigiCert Assured ID Root CA"
@ -3371,46 +3150,6 @@ DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ
+RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A= +RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A=
-----END CERTIFICATE----- -----END CERTIFICATE-----
# Issuer: CN=GLOBALTRUST 2020 O=e-commerce monitoring GmbH
# Subject: CN=GLOBALTRUST 2020 O=e-commerce monitoring GmbH
# Label: "GLOBALTRUST 2020"
# Serial: 109160994242082918454945253
# MD5 Fingerprint: 8a:c7:6f:cb:6d:e3:cc:a2:f1:7c:83:fa:0e:78:d7:e8
# SHA1 Fingerprint: d0:67:c1:13:51:01:0c:aa:d0:c7:6a:65:37:31:16:26:4f:53:71:a2
# SHA256 Fingerprint: 9a:29:6a:51:82:d1:d4:51:a2:e3:7f:43:9b:74:da:af:a2:67:52:33:29:f9:0f:9a:0d:20:07:c3:34:e2:3c:9a
-----BEGIN CERTIFICATE-----
MIIFgjCCA2qgAwIBAgILWku9WvtPilv6ZeUwDQYJKoZIhvcNAQELBQAwTTELMAkG
A1UEBhMCQVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkw
FwYDVQQDExBHTE9CQUxUUlVTVCAyMDIwMB4XDTIwMDIxMDAwMDAwMFoXDTQwMDYx
MDAwMDAwMFowTTELMAkGA1UEBhMCQVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9u
aXRvcmluZyBHbWJIMRkwFwYDVQQDExBHTE9CQUxUUlVTVCAyMDIwMIICIjANBgkq
hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAri5WrRsc7/aVj6B3GyvTY4+ETUWiD59b
RatZe1E0+eyLinjF3WuvvcTfk0Uev5E4C64OFudBc/jbu9G4UeDLgztzOG53ig9Z
YybNpyrOVPu44sB8R85gfD+yc/LAGbaKkoc1DZAoouQVBGM+uq/ufF7MpotQsjj3
QWPKzv9pj2gOlTblzLmMCcpL3TGQlsjMH/1WljTbjhzqLL6FLmPdqqmV0/0plRPw
yJiT2S0WR5ARg6I6IqIoV6Lr/sCMKKCmfecqQjuCgGOlYx8ZzHyyZqjC0203b+J+
BlHZRYQfEs4kUmSFC0iAToexIiIwquuuvuAC4EDosEKAA1GqtH6qRNdDYfOiaxaJ
SaSjpCuKAsR49GiKweR6NrFvG5Ybd0mN1MkGco/PU+PcF4UgStyYJ9ORJitHHmkH
r96i5OTUawuzXnzUJIBHKWk7buis/UDr2O1xcSvy6Fgd60GXIsUf1DnQJ4+H4xj0
4KlGDfV0OoIu0G4skaMxXDtG6nsEEFZegB31pWXogvziB4xiRfUg3kZwhqG8k9Me
dKZssCz3AwyIDMvUclOGvGBG85hqwvG/Q/lwIHfKN0F5VVJjjVsSn8VoxIidrPIw
q7ejMZdnrY8XD2zHc+0klGvIg5rQmjdJBKuxFshsSUktq6HQjJLyQUp5ISXbY9e2
nKd+Qmn7OmMCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
AQYwHQYDVR0OBBYEFNwuH9FhN3nkq9XVsxJxaD1qaJwiMB8GA1UdIwQYMBaAFNwu
H9FhN3nkq9XVsxJxaD1qaJwiMA0GCSqGSIb3DQEBCwUAA4ICAQCR8EICaEDuw2jA
VC/f7GLDw56KoDEoqoOOpFaWEhCGVrqXctJUMHytGdUdaG/7FELYjQ7ztdGl4wJC
XtzoRlgHNQIw4Lx0SsFDKv/bGtCwr2zD/cuz9X9tAy5ZVp0tLTWMstZDFyySCstd
6IwPS3BD0IL/qMy/pJTAvoe9iuOTe8aPmxadJ2W8esVCgmxcB9CpwYhgROmYhRZf
+I/KARDOJcP5YBugxZfD0yyIMaK9MOzQ0MAS8cE54+X1+NZK3TTN+2/BT+MAi1bi
kvcoskJ3ciNnxz8RFbLEAwW+uxF7Cr+obuf/WEPPm2eggAe2HcqtbepBEX4tdJP7
wry+UUTF72glJ4DjyKDUEuzZpTcdN3y0kcra1LGWge9oXHYQSa9+pTeAsRxSvTOB
TI/53WXZFM2KJVj04sWDpQmQ1GwUY7VA3+vA/MRYfg0UFodUJ25W5HCEuGwyEn6C
MUO+1918oa2u1qsgEu8KwxCMSZY13At1XrFP1U80DhEgB3VDRemjEdqso5nCtnkn
4rnvyOL2NSl6dPrFf4IFYqYK6miyeUcGbvJXqBUzxvd4Sj1Ce2t+/vdG6tHrju+I
aFvowdlxfv1k7/9nR4hYJS8+hge9+6jlgqispdNpQ80xiEmEU5LAsTkbOYMBMMTy
qfrQA71yN2BWHzZ8vTmR9W0Nv3vXkg==
-----END CERTIFICATE-----
# Issuer: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz # Issuer: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz
# Subject: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz # Subject: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz
# Label: "ANF Secure Server Root CA" # Label: "ANF Secure Server Root CA"
@ -4855,6 +4594,68 @@ knCDgKs4qllo3UCkGJCy89UDyibK79XH4I9TjvAA46jtn/mtd+ArY0+ew+43u3gJ
hJ65bvspmZDogNOfJA== hJ65bvspmZDogNOfJA==
-----END CERTIFICATE----- -----END CERTIFICATE-----
# Issuer: CN=TrustAsia TLS ECC Root CA O=TrustAsia Technologies, Inc.
# Subject: CN=TrustAsia TLS ECC Root CA O=TrustAsia Technologies, Inc.
# Label: "TrustAsia TLS ECC Root CA"
# Serial: 310892014698942880364840003424242768478804666567
# MD5 Fingerprint: 09:48:04:77:d2:fc:65:93:71:66:b1:11:95:4f:06:8c
# SHA1 Fingerprint: b5:ec:39:f3:a1:66:37:ae:c3:05:94:57:e2:be:11:be:b7:a1:7f:36
# SHA256 Fingerprint: c0:07:6b:9e:f0:53:1f:b1:a6:56:d6:7c:4e:be:97:cd:5d:ba:a4:1e:f4:45:98:ac:c2:48:98:78:c9:2d:87:11
-----BEGIN CERTIFICATE-----
MIICMTCCAbegAwIBAgIUNnThTXxlE8msg1UloD5Sfi9QaMcwCgYIKoZIzj0EAwMw
WDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs
IEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgRUNDIFJvb3QgQ0EwHhcNMjQw
NTE1MDU0MTU2WhcNNDQwNTE1MDU0MTU1WjBYMQswCQYDVQQGEwJDTjElMCMGA1UE
ChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1c3RB
c2lhIFRMUyBFQ0MgUm9vdCBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLh/pVs/
AT598IhtrimY4ZtcU5nb9wj/1WrgjstEpvDBjL1P1M7UiFPoXlfXTr4sP/MSpwDp
guMqWzJ8S5sUKZ74LYO1644xST0mYekdcouJtgq7nDM1D9rs3qlKH8kzsaNCMEAw
DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQULIVTu7FDzTLqnqOH/qKYqKaT6RAw
DgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMFRH18MtYYZI9HlaVQ01
L18N9mdsd0AaRuf4aFtOJx24mH1/k78ITcTaRTChD15KeAIxAKORh/IRM4PDwYqR
OkwrULG9IpRdNYlzg8WbGf60oenUoWa2AaU2+dhoYSi3dOGiMQ==
-----END CERTIFICATE-----
# Issuer: CN=TrustAsia TLS RSA Root CA O=TrustAsia Technologies, Inc.
# Subject: CN=TrustAsia TLS RSA Root CA O=TrustAsia Technologies, Inc.
# Label: "TrustAsia TLS RSA Root CA"
# Serial: 160405846464868906657516898462547310235378010780
# MD5 Fingerprint: 3b:9e:c3:86:0f:34:3c:6b:c5:46:c4:8e:1d:e7:19:12
# SHA1 Fingerprint: a5:46:50:c5:62:ea:95:9a:1a:a7:04:6f:17:58:c7:29:53:3d:03:fa
# SHA256 Fingerprint: 06:c0:8d:7d:af:d8:76:97:1e:b1:12:4f:e6:7f:84:7e:c0:c7:a1:58:d3:ea:53:cb:e9:40:e2:ea:97:91:f4:c3
-----BEGIN CERTIFICATE-----
MIIFgDCCA2igAwIBAgIUHBjYz+VTPyI1RlNUJDxsR9FcSpwwDQYJKoZIhvcNAQEM
BQAwWDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dp
ZXMsIEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgUlNBIFJvb3QgQ0EwHhcN
MjQwNTE1MDU0MTU3WhcNNDQwNTE1MDU0MTU2WjBYMQswCQYDVQQGEwJDTjElMCMG
A1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1
c3RBc2lhIFRMUyBSU0EgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC
AgoCggIBAMMWuBtqpERz5dZO9LnPWwvB0ZqB9WOwj0PBuwhaGnrhB3YmH49pVr7+
NmDQDIPNlOrnxS1cLwUWAp4KqC/lYCZUlviYQB2srp10Zy9U+5RjmOMmSoPGlbYJ
Q1DNDX3eRA5gEk9bNb2/mThtfWza4mhzH/kxpRkQcwUqwzIZheo0qt1CHjCNP561
HmHVb70AcnKtEj+qpklz8oYVlQwQX1Fkzv93uMltrOXVmPGZLmzjyUT5tUMnCE32
ft5EebuyjBza00tsLtbDeLdM1aTk2tyKjg7/D8OmYCYozza/+lcK7Fs/6TAWe8Tb
xNRkoDD75f0dcZLdKY9BWN4ArTr9PXwaqLEX8E40eFgl1oUh63kd0Nyrz2I8sMeX
i9bQn9P+PN7F4/w6g3CEIR0JwqH8uyghZVNgepBtljhb//HXeltt08lwSUq6HTrQ
UNoyIBnkiz/r1RYmNzz7dZ6wB3C4FGB33PYPXFIKvF1tjVEK2sUYyJtt3LCDs3+j
TnhMmCWr8n4uIF6CFabW2I+s5c0yhsj55NqJ4js+k8UTav/H9xj8Z7XvGCxUq0DT
bE3txci3OE9kxJRMT6DNrqXGJyV1J23G2pyOsAWZ1SgRxSHUuPzHlqtKZFlhaxP8
S8ySpg+kUb8OWJDZgoM5pl+z+m6Ss80zDoWo8SnTq1mt1tve1CuBAgMBAAGjQjBA
MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLgHkXlcBvRG/XtZylomkadFK/hT
MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQwFAAOCAgEAIZtqBSBdGBanEqT3
Rz/NyjuujsCCztxIJXgXbODgcMTWltnZ9r96nBO7U5WS/8+S4PPFJzVXqDuiGev4
iqME3mmL5Dw8veWv0BIb5Ylrc5tvJQJLkIKvQMKtuppgJFqBTQUYo+IzeXoLH5Pt
7DlK9RME7I10nYEKqG/odv6LTytpEoYKNDbdgptvT+Bz3Ul/KD7JO6NXBNiT2Twp
2xIQaOHEibgGIOcberyxk2GaGUARtWqFVwHxtlotJnMnlvm5P1vQiJ3koP26TpUJ
g3933FEFlJ0gcXax7PqJtZwuhfG5WyRasQmr2soaB82G39tp27RIGAAtvKLEiUUj
pQ7hRGU+isFqMB3iYPg6qocJQrmBktwliJiJ8Xw18WLK7nn4GS/+X/jbh87qqA8M
pugLoDzga5SYnH+tBuYc6kIQX+ImFTw3OffXvO645e8D7r0i+yiGNFjEWn9hongP
XvPKnbwbPKfILfanIhHKA9jnZwqKDss1jjQ52MjqjZ9k4DewbNfFj8GQYSbbJIwe
SsCI3zWQzj8C9GRh3sfIB5XeMhg6j6JCQCTl1jNdfK7vsU1P1FeQNWrcrgSXSYk0
ly4wBOeY99sLAZDBHwo/+ML+TvrbmnNzFrwFuHnYWa8G5z9nODmxfKuU4CkUpijy
323imttUQ/hHWKNddBWcwauwxzQ=
-----END CERTIFICATE-----
# Issuer: CN=D-TRUST EV Root CA 2 2023 O=D-Trust GmbH # Issuer: CN=D-TRUST EV Root CA 2 2023 O=D-Trust GmbH
# Subject: CN=D-TRUST EV Root CA 2 2023 O=D-Trust GmbH # Subject: CN=D-TRUST EV Root CA 2 2023 O=D-Trust GmbH
# Label: "D-TRUST EV Root CA 2 2023" # Label: "D-TRUST EV Root CA 2 2023"
@ -4895,3 +4696,43 @@ gofXNJhuS5N5YHVpD/Aa1VP6IQzCP+k/HxiMkl14p3ZnGbuy6n/pcAlWVqOwDAst
Nl7F6cTVg8uGF5csbBNvh1qvSaYd2804BC5f4ko1Di1L+KIkBI3Y4WNeApI02phh Nl7F6cTVg8uGF5csbBNvh1qvSaYd2804BC5f4ko1Di1L+KIkBI3Y4WNeApI02phh
XBxvWHZks/wCuPWdCg== XBxvWHZks/wCuPWdCg==
-----END CERTIFICATE----- -----END CERTIFICATE-----
# Issuer: CN=SwissSign RSA TLS Root CA 2022 - 1 O=SwissSign AG
# Subject: CN=SwissSign RSA TLS Root CA 2022 - 1 O=SwissSign AG
# Label: "SwissSign RSA TLS Root CA 2022 - 1"
# Serial: 388078645722908516278762308316089881486363258315
# MD5 Fingerprint: 16:2e:e4:19:76:81:85:ba:8e:91:58:f1:15:ef:72:39
# SHA1 Fingerprint: 81:34:0a:be:4c:cd:ce:cc:e7:7d:cc:8a:d4:57:e2:45:a0:77:5d:ce
# SHA256 Fingerprint: 19:31:44:f4:31:e0:fd:db:74:07:17:d4:de:92:6a:57:11:33:88:4b:43:60:d3:0e:27:29:13:cb:e6:60:ce:41
-----BEGIN CERTIFICATE-----
MIIFkzCCA3ugAwIBAgIUQ/oMX04bgBhE79G0TzUfRPSA7cswDQYJKoZIhvcNAQEL
BQAwUTELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzErMCkGA1UE
AxMiU3dpc3NTaWduIFJTQSBUTFMgUm9vdCBDQSAyMDIyIC0gMTAeFw0yMjA2MDgx
MTA4MjJaFw00NzA2MDgxMTA4MjJaMFExCzAJBgNVBAYTAkNIMRUwEwYDVQQKEwxT
d2lzc1NpZ24gQUcxKzApBgNVBAMTIlN3aXNzU2lnbiBSU0EgVExTIFJvb3QgQ0Eg
MjAyMiAtIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDLKmjiC8NX
vDVjvHClO/OMPE5Xlm7DTjak9gLKHqquuN6orx122ro10JFwB9+zBvKK8i5VUXu7
LCTLf5ImgKO0lPaCoaTo+nUdWfMHamFk4saMla+ju45vVs9xzF6BYQ1t8qsCLqSX
5XH8irCRIFucdFJtrhUnWXjyCcplDn/L9Ovn3KlMd/YrFgSVrpxxpT8q2kFC5zyE
EPThPYxr4iuRR1VPuFa+Rd4iUU1OKNlfGUEGjw5NBuBwQCMBauTLE5tzrE0USJIt
/m2n+IdreXXhvhCxqohAWVTXz8TQm0SzOGlkjIHRI36qOTw7D59Ke4LKa2/KIj4x
0LDQKhySio/YGZxH5D4MucLNvkEM+KRHBdvBFzA4OmnczcNpI/2aDwLOEGrOyvi5
KaM2iYauC8BPY7kGWUleDsFpswrzd34unYyzJ5jSmY0lpx+Gs6ZUcDj8fV3oT4MM
0ZPlEuRU2j7yrTrePjxF8CgPBrnh25d7mUWe3f6VWQQvdT/TromZhqwUtKiE+shd
OxtYk8EXlFXIC+OCeYSf8wCENO7cMdWP8vpPlkwGqnj73mSiI80fPsWMvDdUDrta
clXvyFu1cvh43zcgTFeRc5JzrBh3Q4IgaezprClG5QtO+DdziZaKHG29777YtvTK
wP1H8K4LWCDFyB02rpeNUIMmJCn3nTsPBQIDAQABo2MwYTAPBgNVHRMBAf8EBTAD
AQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBRvjmKLk0Ow4UD2p8P98Q+4
DxU4pTAdBgNVHQ4EFgQUb45ii5NDsOFA9qfD/fEPuA8VOKUwDQYJKoZIhvcNAQEL
BQADggIBAKwsKUF9+lz1GpUYvyypiqkkVHX1uECry6gkUSsYP2OprphWKwVDIqO3
10aewCoSPY6WlkDfDDOLazeROpW7OSltwAJsipQLBwJNGD77+3v1dj2b9l4wBlgz
Hqp41eZUBDqyggmNzhYzWUUo8aWjlw5DI/0LIICQ/+Mmz7hkkeUFjxOgdg3XNwwQ
iJb0Pr6VvfHDffCjw3lHC1ySFWPtUnWK50Zpy1FVCypM9fJkT6lc/2cyjlUtMoIc
gC9qkfjLvH4YoiaoLqNTKIftV+Vlek4ASltOU8liNr3CjlvrzG4ngRhZi0Rjn9UM
ZfQpZX+RLOV/fuiJz48gy20HQhFRJjKKLjpHE7iNvUcNCfAWpO2Whi4Z2L6MOuhF
LhG6rlrnub+xzI/goP+4s9GFe3lmozm1O2bYQL7Pt2eLSMkZJVX8vY3PXtpOpvJp
zv1/THfQwUY1mFwjmwJFQ5Ra3bxHrSL+ul4vkSkphnsh3m5kt8sNjzdbowhq6/Td
Ao9QAwKxuDdollDruF/UKIqlIgyKhPBZLtU30WHlQnNYKoH3dtvi4k0NX/a3vgW0
rk4N3hY9A4GzJl5LuEsAz/+MF7psYC0nhzck5npgL7XTgwSqT0N1osGDsieYK7EO
gLrAhV5Cud+xYJHT6xh+cHiudoO+cVrQkOPKwRYlZ0rwtnu64ZzZ
-----END CERTIFICATE-----

View file

@ -46,7 +46,7 @@ if sys.version_info >= (3, 11):
def contents() -> str: def contents() -> str:
return files("certifi").joinpath("cacert.pem").read_text(encoding="ascii") return files("certifi").joinpath("cacert.pem").read_text(encoding="ascii")
elif sys.version_info >= (3, 7): else:
from importlib.resources import path as get_path, read_text from importlib.resources import path as get_path, read_text
@ -81,34 +81,3 @@ elif sys.version_info >= (3, 7):
def contents() -> str: def contents() -> str:
return read_text("certifi", "cacert.pem", encoding="ascii") return read_text("certifi", "cacert.pem", encoding="ascii")
else:
import os
import types
from typing import Union
Package = Union[types.ModuleType, str]
Resource = Union[str, "os.PathLike"]
# This fallback will work for Python versions prior to 3.7 that lack the
# importlib.resources module but relies on the existing `where` function
# so won't address issues with environments like PyOxidizer that don't set
# __file__ on modules.
def read_text(
package: Package,
resource: Resource,
encoding: str = 'utf-8',
errors: str = 'strict'
) -> str:
with open(where(), encoding=encoding) as data:
return data.read()
# If we don't have importlib.resources, then we will just do the old logic
# of assuming we're on the filesystem and munge the path directly.
def where() -> str:
f = os.path.dirname(__file__)
return os.path.join(f, "cacert.pem")
def contents() -> str:
return read_text("certifi", "cacert.pem", encoding="ascii")

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,13 @@
# file generated by setuptools_scm # file generated by setuptools-scm
# don't change, don't track in version control # don't change, don't track in version control
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
TYPE_CHECKING = False TYPE_CHECKING = False
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Tuple, Union from typing import Tuple
from typing import Union
VERSION_TUPLE = Tuple[Union[int, str], ...] VERSION_TUPLE = Tuple[Union[int, str], ...]
else: else:
VERSION_TUPLE = object VERSION_TUPLE = object
@ -12,5 +17,5 @@ __version__: str
__version_tuple__: VERSION_TUPLE __version_tuple__: VERSION_TUPLE
version_tuple: VERSION_TUPLE version_tuple: VERSION_TUPLE
__version__ = version = '2.3.0' __version__ = version = '2.5.0'
__version_tuple__ = version_tuple = (2, 3, 0) __version_tuple__ = version_tuple = (2, 5, 0)

View file

@ -74,7 +74,7 @@ port_by_scheme = {"http": 80, "https": 443}
# When it comes time to update this value as a part of regular maintenance # When it comes time to update this value as a part of regular maintenance
# (ie test_recent_date is failing) update it to ~6 months before the current date. # (ie test_recent_date is failing) update it to ~6 months before the current date.
RECENT_DATE = datetime.date(2023, 6, 1) RECENT_DATE = datetime.date(2025, 1, 1)
_CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]") _CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]")
@ -232,12 +232,22 @@ class HTTPConnection(_HTTPConnection):
super().set_tunnel(host, port=port, headers=headers) super().set_tunnel(host, port=port, headers=headers)
self._tunnel_scheme = scheme self._tunnel_scheme = scheme
if sys.version_info < (3, 11, 4): if sys.version_info < (3, 11, 9) or ((3, 12) <= sys.version_info < (3, 12, 3)):
# Taken from python/cpython#100986 which was backported in 3.11.9 and 3.12.3.
# When using connection_from_host, host will come without brackets.
def _wrap_ipv6(self, ip: bytes) -> bytes:
if b":" in ip and ip[0] != b"["[0]:
return b"[" + ip + b"]"
return ip
if sys.version_info < (3, 11, 9):
# `_tunnel` copied from 3.11.13 backporting
# https://github.com/python/cpython/commit/0d4026432591d43185568dd31cef6a034c4b9261
# and https://github.com/python/cpython/commit/6fbc61070fda2ffb8889e77e3b24bca4249ab4d1
def _tunnel(self) -> None: def _tunnel(self) -> None:
_MAXLINE = http.client._MAXLINE # type: ignore[attr-defined] _MAXLINE = http.client._MAXLINE # type: ignore[attr-defined]
connect = b"CONNECT %s:%d HTTP/1.0\r\n" % ( # type: ignore[str-format] connect = b"CONNECT %s:%d HTTP/1.0\r\n" % ( # type: ignore[str-format]
self._tunnel_host.encode("ascii"), # type: ignore[union-attr] self._wrap_ipv6(self._tunnel_host.encode("ascii")), # type: ignore[union-attr]
self._tunnel_port, self._tunnel_port,
) )
headers = [connect] headers = [connect]
@ -256,7 +266,9 @@ class HTTPConnection(_HTTPConnection):
if code != http.HTTPStatus.OK: if code != http.HTTPStatus.OK:
self.close() self.close()
raise OSError(f"Tunnel connection failed: {code} {message.strip()}") raise OSError(
f"Tunnel connection failed: {code} {message.strip()}"
)
while True: while True:
line = response.fp.readline(_MAXLINE + 1) line = response.fp.readline(_MAXLINE + 1)
if len(line) > _MAXLINE: if len(line) > _MAXLINE:
@ -272,6 +284,43 @@ class HTTPConnection(_HTTPConnection):
finally: finally:
response.close() response.close()
elif (3, 12) <= sys.version_info < (3, 12, 3):
# `_tunnel` copied from 3.12.11 backporting
# https://github.com/python/cpython/commit/23aef575c7629abcd4aaf028ebd226fb41a4b3c8
def _tunnel(self) -> None: # noqa: F811
connect = b"CONNECT %s:%d HTTP/1.1\r\n" % ( # type: ignore[str-format]
self._wrap_ipv6(self._tunnel_host.encode("idna")), # type: ignore[union-attr]
self._tunnel_port,
)
headers = [connect]
for header, value in self._tunnel_headers.items(): # type: ignore[attr-defined]
headers.append(f"{header}: {value}\r\n".encode("latin-1"))
headers.append(b"\r\n")
# Making a single send() call instead of one per line encourages
# the host OS to use a more optimal packet size instead of
# potentially emitting a series of small packets.
self.send(b"".join(headers))
del headers
response = self.response_class(self.sock, method=self._method) # type: ignore[attr-defined]
try:
(version, code, message) = response._read_status() # type: ignore[attr-defined]
self._raw_proxy_headers = http.client._read_headers(response.fp) # type: ignore[attr-defined]
if self.debuglevel > 0:
for header in self._raw_proxy_headers:
print("header:", header.decode())
if code != http.HTTPStatus.OK:
self.close()
raise OSError(
f"Tunnel connection failed: {code} {message.strip()}"
)
finally:
response.close()
def connect(self) -> None: def connect(self) -> None:
self.sock = self._new_conn() self.sock = self._new_conn()
if self._tunnel_host: if self._tunnel_host:

View file

@ -573,6 +573,11 @@ def send_jspi_request(
"method": request.method, "method": request.method,
"signal": js_abort_controller.signal, "signal": js_abort_controller.signal,
} }
# Node.js returns the whole response (unlike opaqueredirect in browsers),
# so urllib3 can set `redirect: manual` to control redirects itself.
# https://stackoverflow.com/a/78524615
if _is_node_js():
fetch_data["redirect"] = "manual"
# Call JavaScript fetch (async api, returns a promise) # Call JavaScript fetch (async api, returns a promise)
fetcher_promise_js = js.fetch(request.url, _obj_from_dict(fetch_data)) fetcher_promise_js = js.fetch(request.url, _obj_from_dict(fetch_data))
# Now suspend WebAssembly until we resolve that promise # Now suspend WebAssembly until we resolve that promise
@ -693,6 +698,21 @@ def has_jspi() -> bool:
return False return False
def _is_node_js() -> bool:
"""
Check if we are in Node.js.
:return: True if we are in Node.js.
:rtype: bool
"""
return (
hasattr(js, "process")
and hasattr(js.process, "release")
# According to the Node.js documentation, the release name is always "node".
and js.process.release.name == "node"
)
def streaming_ready() -> bool | None: def streaming_ready() -> bool | None:
if _fetcher: if _fetcher:
return _fetcher.streaming_ready return _fetcher.streaming_ready

View file

@ -160,14 +160,6 @@ class EmscriptenHttpResponseWrapper(BaseHTTPResponse):
# don't cache partial content # don't cache partial content
cache_content = False cache_content = False
data = self._response.body.read(amt) data = self._response.body.read(amt)
if self.length_remaining is not None:
self.length_remaining = max(self.length_remaining - len(data), 0)
if (self.length_is_certain and self.length_remaining == 0) or len(
data
) < amt:
# definitely finished reading, close response stream
self._response.body.close()
return typing.cast(bytes, data)
else: # read all we can (and cache it) else: # read all we can (and cache it)
data = self._response.body.read() data = self._response.body.read()
if cache_content: if cache_content:

View file

@ -424,6 +424,7 @@ class PyOpenSSLContext:
self.check_hostname = False self.check_hostname = False
self._minimum_version: int = ssl.TLSVersion.MINIMUM_SUPPORTED self._minimum_version: int = ssl.TLSVersion.MINIMUM_SUPPORTED
self._maximum_version: int = ssl.TLSVersion.MAXIMUM_SUPPORTED self._maximum_version: int = ssl.TLSVersion.MAXIMUM_SUPPORTED
self._verify_flags: int = ssl.VERIFY_X509_TRUSTED_FIRST
@property @property
def options(self) -> int: def options(self) -> int:
@ -434,6 +435,15 @@ class PyOpenSSLContext:
self._options = value self._options = value
self._set_ctx_options() self._set_ctx_options()
@property
def verify_flags(self) -> int:
return self._verify_flags
@verify_flags.setter
def verify_flags(self, value: int) -> None:
self._verify_flags = value
self._ctx.get_cert_store().set_flags(self._verify_flags)
@property @property
def verify_mode(self) -> int: def verify_mode(self) -> int:
return _openssl_to_stdlib_verify[self._ctx.get_verify_mode()] return _openssl_to_stdlib_verify[self._ctx.get_verify_mode()]

View file

@ -31,11 +31,12 @@ class PoolError(HTTPError):
def __init__(self, pool: ConnectionPool, message: str) -> None: def __init__(self, pool: ConnectionPool, message: str) -> None:
self.pool = pool self.pool = pool
self._message = message
super().__init__(f"{pool}: {message}") super().__init__(f"{pool}: {message}")
def __reduce__(self) -> _TYPE_REDUCE_RESULT: def __reduce__(self) -> _TYPE_REDUCE_RESULT:
# For pickling purposes. # For pickling purposes.
return self.__class__, (None, None) return self.__class__, (None, self._message)
class RequestError(PoolError): class RequestError(PoolError):
@ -47,7 +48,7 @@ class RequestError(PoolError):
def __reduce__(self) -> _TYPE_REDUCE_RESULT: def __reduce__(self) -> _TYPE_REDUCE_RESULT:
# For pickling purposes. # For pickling purposes.
return self.__class__, (None, self.url, None) return self.__class__, (None, self.url, self._message)
class SSLError(HTTPError): class SSLError(HTTPError):
@ -100,6 +101,10 @@ class MaxRetryError(RequestError):
super().__init__(pool, url, message) super().__init__(pool, url, message)
def __reduce__(self) -> _TYPE_REDUCE_RESULT:
# For pickling purposes.
return self.__class__, (None, self.url, self.reason)
class HostChangedError(RequestError): class HostChangedError(RequestError):
"""Raised when an existing pool gets a request for a foreign host.""" """Raised when an existing pool gets a request for a foreign host."""
@ -139,11 +144,12 @@ class NewConnectionError(ConnectTimeoutError, HTTPError):
def __init__(self, conn: HTTPConnection, message: str) -> None: def __init__(self, conn: HTTPConnection, message: str) -> None:
self.conn = conn self.conn = conn
self._message = message
super().__init__(f"{conn}: {message}") super().__init__(f"{conn}: {message}")
def __reduce__(self) -> _TYPE_REDUCE_RESULT: def __reduce__(self) -> _TYPE_REDUCE_RESULT:
# For pickling purposes. # For pickling purposes.
return self.__class__, (None, None) return self.__class__, (None, self._message)
@property @property
def pool(self) -> HTTPConnection: def pool(self) -> HTTPConnection:
@ -162,11 +168,13 @@ class NameResolutionError(NewConnectionError):
def __init__(self, host: str, conn: HTTPConnection, reason: socket.gaierror): def __init__(self, host: str, conn: HTTPConnection, reason: socket.gaierror):
message = f"Failed to resolve '{host}' ({reason})" message = f"Failed to resolve '{host}' ({reason})"
self._host = host
self._reason = reason
super().__init__(conn, message) super().__init__(conn, message)
def __reduce__(self) -> _TYPE_REDUCE_RESULT: def __reduce__(self) -> _TYPE_REDUCE_RESULT:
# For pickling purposes. # For pickling purposes.
return self.__class__, (None, None, None) return self.__class__, (self._host, None, self._reason)
class EmptyPoolError(PoolError): class EmptyPoolError(PoolError):

View file

@ -140,7 +140,7 @@ class HTTP2Connection(HTTPSConnection):
with self._h2_conn as conn: with self._h2_conn as conn:
self._h2_stream = conn.get_next_available_stream_id() self._h2_stream = conn.get_next_available_stream_id()
def putheader(self, header: str | bytes, *values: str | bytes) -> None: def putheader(self, header: str | bytes, *values: str | bytes) -> None: # type: ignore[override]
# TODO SKIPPABLE_HEADERS from urllib3 are ignored. # TODO SKIPPABLE_HEADERS from urllib3 are ignored.
header = header.encode() if isinstance(header, str) else header header = header.encode() if isinstance(header, str) else header
header = header.lower() # A lot of upstream code uses capitalized headers. header = header.lower() # A lot of upstream code uses capitalized headers.

View file

@ -203,6 +203,22 @@ class PoolManager(RequestMethods):
**connection_pool_kw: typing.Any, **connection_pool_kw: typing.Any,
) -> None: ) -> None:
super().__init__(headers) super().__init__(headers)
if "retries" in connection_pool_kw:
retries = connection_pool_kw["retries"]
if not isinstance(retries, Retry):
# When Retry is initialized, raise_on_redirect is based
# on a redirect boolean value.
# But requests made via a pool manager always set
# redirect to False, and raise_on_redirect always ends
# up being False consequently.
# Here we fix the issue by setting raise_on_redirect to
# a value needed by the pool manager without considering
# the redirect boolean.
raise_on_redirect = retries is not False
retries = Retry.from_int(retries, redirect=False)
retries.raise_on_redirect = raise_on_redirect
connection_pool_kw = connection_pool_kw.copy()
connection_pool_kw["retries"] = retries
self.connection_pool_kw = connection_pool_kw self.connection_pool_kw = connection_pool_kw
self.pools: RecentlyUsedContainer[PoolKey, HTTPConnectionPool] self.pools: RecentlyUsedContainer[PoolKey, HTTPConnectionPool]
@ -456,7 +472,7 @@ class PoolManager(RequestMethods):
kw["body"] = None kw["body"] = None
kw["headers"] = HTTPHeaderDict(kw["headers"])._prepare_for_method_change() kw["headers"] = HTTPHeaderDict(kw["headers"])._prepare_for_method_change()
retries = kw.get("retries") retries = kw.get("retries", response.retries)
if not isinstance(retries, Retry): if not isinstance(retries, Retry):
retries = Retry.from_int(retries, redirect=redirect) retries = Retry.from_int(retries, redirect=redirect)

View file

@ -26,23 +26,6 @@ try:
except ImportError: except ImportError:
brotli = None brotli = None
try:
import zstandard as zstd
except (AttributeError, ImportError, ValueError): # Defensive:
HAS_ZSTD = False
else:
# The package 'zstandard' added the 'eof' property starting
# in v0.18.0 which we require to ensure a complete and
# valid zstd stream was fed into the ZstdDecoder.
# See: https://github.com/urllib3/urllib3/pull/2624
_zstd_version = tuple(
map(int, re.search(r"^([0-9]+)\.([0-9]+)", zstd.__version__).groups()) # type: ignore[union-attr]
)
if _zstd_version < (0, 18): # Defensive:
HAS_ZSTD = False
else:
HAS_ZSTD = True
from . import util from . import util
from ._base_connection import _TYPE_BODY from ._base_connection import _TYPE_BODY
from ._collections import HTTPHeaderDict from ._collections import HTTPHeaderDict
@ -163,9 +146,51 @@ if brotli is not None:
return b"" return b""
if HAS_ZSTD: try:
# Python 3.14+
from compression import zstd # type: ignore[import-not-found] # noqa: F401
HAS_ZSTD = True
class ZstdDecoder(ContentDecoder): class ZstdDecoder(ContentDecoder):
def __init__(self) -> None:
self._obj = zstd.ZstdDecompressor()
def decompress(self, data: bytes) -> bytes:
if not data:
return b""
data_parts = [self._obj.decompress(data)]
while self._obj.eof and self._obj.unused_data:
unused_data = self._obj.unused_data
self._obj = zstd.ZstdDecompressor()
data_parts.append(self._obj.decompress(unused_data))
return b"".join(data_parts)
def flush(self) -> bytes:
if not self._obj.eof:
raise DecodeError("Zstandard data is incomplete")
return b""
except ImportError:
try:
# Python 3.13 and earlier require the 'zstandard' module.
import zstandard as zstd
# The package 'zstandard' added the 'eof' property starting
# in v0.18.0 which we require to ensure a complete and
# valid zstd stream was fed into the ZstdDecoder.
# See: https://github.com/urllib3/urllib3/pull/2624
_zstd_version = tuple(
map(int, re.search(r"^([0-9]+)\.([0-9]+)", zstd.__version__).groups()) # type: ignore[union-attr]
)
if _zstd_version < (0, 18): # Defensive:
raise ImportError("zstandard module doesn't have eof")
except (AttributeError, ImportError, ValueError): # Defensive:
HAS_ZSTD = False
else:
HAS_ZSTD = True
class ZstdDecoder(ContentDecoder): # type: ignore[no-redef]
def __init__(self) -> None: def __init__(self) -> None:
self._obj = zstd.ZstdDecompressor().decompressobj() self._obj = zstd.ZstdDecompressor().decompressobj()
@ -183,7 +208,7 @@ if HAS_ZSTD:
ret = self._obj.flush() # note: this is a no-op ret = self._obj.flush() # note: this is a no-op
if not self._obj.eof: if not self._obj.eof:
raise DecodeError("Zstandard data is incomplete") raise DecodeError("Zstandard data is incomplete")
return ret return ret # type: ignore[no-any-return]
class MultiDecoder(ContentDecoder): class MultiDecoder(ContentDecoder):
@ -518,7 +543,7 @@ class BaseHTTPResponse(io.IOBase):
def getheaders(self) -> HTTPHeaderDict: def getheaders(self) -> HTTPHeaderDict:
warnings.warn( warnings.warn(
"HTTPResponse.getheaders() is deprecated and will be removed " "HTTPResponse.getheaders() is deprecated and will be removed "
"in urllib3 v2.1.0. Instead access HTTPResponse.headers directly.", "in urllib3 v2.6.0. Instead access HTTPResponse.headers directly.",
category=DeprecationWarning, category=DeprecationWarning,
stacklevel=2, stacklevel=2,
) )
@ -527,7 +552,7 @@ class BaseHTTPResponse(io.IOBase):
def getheader(self, name: str, default: str | None = None) -> str | None: def getheader(self, name: str, default: str | None = None) -> str | None:
warnings.warn( warnings.warn(
"HTTPResponse.getheader() is deprecated and will be removed " "HTTPResponse.getheader() is deprecated and will be removed "
"in urllib3 v2.1.0. Instead use HTTPResponse.headers.get(name, default).", "in urllib3 v2.6.0. Instead use HTTPResponse.headers.get(name, default).",
category=DeprecationWarning, category=DeprecationWarning,
stacklevel=2, stacklevel=2,
) )
@ -1075,6 +1100,10 @@ class HTTPResponse(BaseHTTPResponse):
def shutdown(self) -> None: def shutdown(self) -> None:
if not self._sock_shutdown: if not self._sock_shutdown:
raise ValueError("Cannot shutdown socket as self._sock_shutdown is not set") raise ValueError("Cannot shutdown socket as self._sock_shutdown is not set")
if self._connection is None:
raise RuntimeError(
"Cannot shutdown as connection has already been released to the pool"
)
self._sock_shutdown(socket.SHUT_RD) self._sock_shutdown(socket.SHUT_RD)
def close(self) -> None: def close(self) -> None:

View file

@ -28,12 +28,20 @@ except ImportError:
pass pass
else: else:
ACCEPT_ENCODING += ",br" ACCEPT_ENCODING += ",br"
try: try:
import zstandard as _unused_module_zstd # noqa: F401 from compression import ( # type: ignore[import-not-found] # noqa: F401
except ImportError: zstd as _unused_module_zstd,
pass )
else:
ACCEPT_ENCODING += ",zstd" ACCEPT_ENCODING += ",zstd"
except ImportError:
try:
import zstandard as _unused_module_zstd # noqa: F401
ACCEPT_ENCODING += ",zstd"
except ImportError:
pass
class _TYPE_FAILEDTELL(Enum): class _TYPE_FAILEDTELL(Enum):

View file

@ -101,6 +101,7 @@ try: # Do we have ssl at all?
OPENSSL_VERSION_NUMBER, OPENSSL_VERSION_NUMBER,
PROTOCOL_TLS, PROTOCOL_TLS,
PROTOCOL_TLS_CLIENT, PROTOCOL_TLS_CLIENT,
VERIFY_X509_STRICT,
OP_NO_SSLv2, OP_NO_SSLv2,
OP_NO_SSLv3, OP_NO_SSLv3,
SSLContext, SSLContext,
@ -109,6 +110,9 @@ try: # Do we have ssl at all?
PROTOCOL_SSLv23 = PROTOCOL_TLS PROTOCOL_SSLv23 = PROTOCOL_TLS
# Needed for Python 3.9 which does not define this
VERIFY_X509_PARTIAL_CHAIN = getattr(ssl, "VERIFY_X509_PARTIAL_CHAIN", 0x80000)
# Setting SSLContext.hostname_checks_common_name = False didn't work before CPython # Setting SSLContext.hostname_checks_common_name = False didn't work before CPython
# 3.9.3, and 3.10 (but OK on PyPy) or OpenSSL 1.1.1l+ # 3.9.3, and 3.10 (but OK on PyPy) or OpenSSL 1.1.1l+
if HAS_NEVER_CHECK_COMMON_NAME and not _is_has_never_check_common_name_reliable( if HAS_NEVER_CHECK_COMMON_NAME and not _is_has_never_check_common_name_reliable(
@ -117,7 +121,7 @@ try: # Do we have ssl at all?
sys.implementation.name, sys.implementation.name,
sys.version_info, sys.version_info,
sys.pypy_version_info if sys.implementation.name == "pypy" else None, # type: ignore[attr-defined] sys.pypy_version_info if sys.implementation.name == "pypy" else None, # type: ignore[attr-defined]
): ): # Defensive: for Python < 3.9.3
HAS_NEVER_CHECK_COMMON_NAME = False HAS_NEVER_CHECK_COMMON_NAME = False
# Need to be careful here in case old TLS versions get # Need to be careful here in case old TLS versions get
@ -138,6 +142,8 @@ except ImportError:
OP_NO_SSLv3 = 0x2000000 # type: ignore[assignment] OP_NO_SSLv3 = 0x2000000 # type: ignore[assignment]
PROTOCOL_SSLv23 = PROTOCOL_TLS = 2 # type: ignore[assignment] PROTOCOL_SSLv23 = PROTOCOL_TLS = 2 # type: ignore[assignment]
PROTOCOL_TLS_CLIENT = 16 # type: ignore[assignment] PROTOCOL_TLS_CLIENT = 16 # type: ignore[assignment]
VERIFY_X509_PARTIAL_CHAIN = 0x80000
VERIFY_X509_STRICT = 0x20 # type: ignore[assignment]
_TYPE_PEER_CERT_RET = typing.Union["_TYPE_PEER_CERT_RET_DICT", bytes, None] _TYPE_PEER_CERT_RET = typing.Union["_TYPE_PEER_CERT_RET_DICT", bytes, None]
@ -223,6 +229,7 @@ def create_urllib3_context(
ciphers: str | None = None, ciphers: str | None = None,
ssl_minimum_version: int | None = None, ssl_minimum_version: int | None = None,
ssl_maximum_version: int | None = None, ssl_maximum_version: int | None = None,
verify_flags: int | None = None,
) -> ssl.SSLContext: ) -> ssl.SSLContext:
"""Creates and configures an :class:`ssl.SSLContext` instance for use with urllib3. """Creates and configures an :class:`ssl.SSLContext` instance for use with urllib3.
@ -247,6 +254,9 @@ def create_urllib3_context(
:param ciphers: :param ciphers:
Which cipher suites to allow the server to select. Defaults to either system configured Which cipher suites to allow the server to select. Defaults to either system configured
ciphers if OpenSSL 1.1.1+, otherwise uses a secure default set of ciphers. ciphers if OpenSSL 1.1.1+, otherwise uses a secure default set of ciphers.
:param verify_flags:
The flags for certificate verification operations. These default to
``ssl.VERIFY_X509_PARTIAL_CHAIN`` and ``ssl.VERIFY_X509_STRICT`` for Python 3.13+.
:returns: :returns:
Constructed SSLContext object with specified options Constructed SSLContext object with specified options
:rtype: SSLContext :rtype: SSLContext
@ -279,7 +289,7 @@ def create_urllib3_context(
# keep the maximum version to be it's default value: 'TLSVersion.MAXIMUM_SUPPORTED' # keep the maximum version to be it's default value: 'TLSVersion.MAXIMUM_SUPPORTED'
warnings.warn( warnings.warn(
"'ssl_version' option is deprecated and will be " "'ssl_version' option is deprecated and will be "
"removed in urllib3 v2.1.0. Instead use 'ssl_minimum_version'", "removed in urllib3 v2.6.0. Instead use 'ssl_minimum_version'",
category=DeprecationWarning, category=DeprecationWarning,
stacklevel=2, stacklevel=2,
) )
@ -320,6 +330,16 @@ def create_urllib3_context(
context.options |= options context.options |= options
if verify_flags is None:
verify_flags = 0
# In Python 3.13+ ssl.create_default_context() sets VERIFY_X509_PARTIAL_CHAIN
# and VERIFY_X509_STRICT so we do the same
if sys.version_info >= (3, 13):
verify_flags |= VERIFY_X509_PARTIAL_CHAIN
verify_flags |= VERIFY_X509_STRICT
context.verify_flags |= verify_flags
# Enable post-handshake authentication for TLS 1.3, see GH #1634. PHA is # Enable post-handshake authentication for TLS 1.3, see GH #1634. PHA is
# necessary for conditional client cert authentication with TLS 1.3. # necessary for conditional client cert authentication with TLS 1.3.
# The attribute is None for OpenSSL <= 1.1.0 or does not exist when using # The attribute is None for OpenSSL <= 1.1.0 or does not exist when using

View file

@ -146,7 +146,7 @@ def match_hostname(
if key == "commonName": if key == "commonName":
if _dnsname_match(value, hostname): if _dnsname_match(value, hostname):
return return
dnsnames.append(value) dnsnames.append(value) # Defensive: for Python < 3.9.3
if len(dnsnames) > 1: if len(dnsnames) > 1:
raise CertificateError( raise CertificateError(

View file

@ -17,8 +17,6 @@ functionality from here and reexpose it in a more focused way.
# dependency loops. The exception is TYPE_CHECKING blocks and # dependency loops. The exception is TYPE_CHECKING blocks and
# annotations since those aren't evaluated at runtime. # annotations since those aren't evaluated at runtime.
# from efro.util import set_canonical_module_names
import _babase import _babase
from _babase import ( from _babase import (
add_clean_frame_callback, add_clean_frame_callback,
@ -30,6 +28,7 @@ from _babase import (
apptime, apptime,
apptimer, apptimer,
AppTimer, AppTimer,
atexit,
asset_loads_allowed, asset_loads_allowed,
fullscreen_control_available, fullscreen_control_available,
fullscreen_control_get, fullscreen_control_get,
@ -59,6 +58,7 @@ from _babase import (
get_replays_dir, get_replays_dir,
get_string_height, get_string_height,
get_string_width, get_string_width,
get_suppress_config_and_state_writes,
get_ui_scale, get_ui_scale,
get_v1_cloud_log_file_path, get_v1_cloud_log_file_path,
get_virtual_safe_area_size, get_virtual_safe_area_size,
@ -69,7 +69,7 @@ from _babase import (
in_logic_thread, in_logic_thread,
in_main_menu, in_main_menu,
increment_analytics_count, increment_analytics_count,
invoke_main_menu, request_main_ui,
is_os_playing_music, is_os_playing_music,
is_xcode_build, is_xcode_build,
lock_all_input, lock_all_input,
@ -79,6 +79,7 @@ from _babase import (
mac_music_app_play_playlist, mac_music_app_play_playlist,
mac_music_app_set_volume, mac_music_app_set_volume,
mac_music_app_stop, mac_music_app_stop,
menu_press,
music_player_play, music_player_play,
music_player_set_volume, music_player_set_volume,
music_player_shutdown, music_player_shutdown,
@ -93,9 +94,9 @@ from _babase import (
overlay_web_browser_is_supported, overlay_web_browser_is_supported,
overlay_web_browser_open_url, overlay_web_browser_open_url,
print_load_info, print_load_info,
push_back_press,
pushcall, pushcall,
quit, quit,
reload_hooks,
reload_media, reload_media,
request_permission, request_permission,
safecolor, safecolor,
@ -103,14 +104,15 @@ from _babase import (
set_analytics_screen, set_analytics_screen,
set_low_level_config_value, set_low_level_config_value,
set_thread_name, set_thread_name,
set_ui_account_state, set_main_ui_input_device,
set_ui_input_device, set_account_sign_in_state,
set_ui_scale, set_ui_scale,
show_progress_bar, show_progress_bar,
shutdown_suppress_begin, shutdown_suppress_begin,
shutdown_suppress_end, shutdown_suppress_end,
shutdown_suppress_count, shutdown_suppress_count,
SimpleSound, SimpleSound,
suppress_config_and_state_writes,
supports_max_fps, supports_max_fps,
supports_vsync, supports_vsync,
supports_unicode_display, supports_unicode_display,
@ -134,7 +136,6 @@ from babase._appconfig import AppConfig
from babase._apputils import ( from babase._apputils import (
handle_leftover_v1_cloud_log_file, handle_leftover_v1_cloud_log_file,
is_browser_likely_available, is_browser_likely_available,
garbage_collect,
get_remote_app_name, get_remote_app_name,
AppHealthSubsystem, AppHealthSubsystem,
utc_now_cloud, utc_now_cloud,
@ -145,6 +146,7 @@ from babase._devconsole import (
DevConsoleTabEntry, DevConsoleTabEntry,
DevConsoleSubsystem, DevConsoleSubsystem,
) )
from babase._discord import DiscordSubsystem
from babase._emptyappmode import EmptyAppMode from babase._emptyappmode import EmptyAppMode
from babase._error import ( from babase._error import (
ContextError, ContextError,
@ -162,6 +164,7 @@ from babase._error import (
SessionNotFoundError, SessionNotFoundError,
DelegateNotFoundError, DelegateNotFoundError,
) )
from babase._gc import GarbageCollectionSubsystem
from babase._general import ( from babase._general import (
DisplayTime, DisplayTime,
AppTime, AppTime,
@ -176,7 +179,7 @@ from babase._general import (
) )
from babase._language import Lstr, LanguageSubsystem from babase._language import Lstr, LanguageSubsystem
from babase._locale import LocaleSubsystem from babase._locale import LocaleSubsystem
from babase._logging import balog, applog, lifecyclelog from babase._logging import balog, accountlog, applog, lifecyclelog, netlog
from babase._login import LoginAdapter, LoginInfo from babase._login import LoginAdapter, LoginInfo
from babase._mgen.enums import ( from babase._mgen.enums import (
@ -188,11 +191,8 @@ from babase._mgen.enums import (
) )
from babase._math import normalized_color, is_point_in_box, vec3validate from babase._math import normalized_color, is_point_in_box, vec3validate
from babase._meta import MetadataSubsystem from babase._meta import MetadataSubsystem
from babase._net import ( from babase._env import DEFAULT_REQUEST_TIMEOUT_SECONDS
get_ip_address_type, from babase._net import get_ip_address_type, NetworkSubsystem
DEFAULT_REQUEST_TIMEOUT_SECONDS,
NetworkSubsystem,
)
from babase._plugin import PluginSpec, Plugin, PluginSubsystem from babase._plugin import PluginSpec, Plugin, PluginSubsystem
from babase._stringedit import StringEditAdapter, StringEditSubsystem from babase._stringedit import StringEditAdapter, StringEditSubsystem
from babase._text import timestring from babase._text import timestring
@ -201,6 +201,7 @@ from babase._workspace import WorkspaceSubsystem
_babase.app = app = App() _babase.app = app = App()
__all__ = [ __all__ = [
'accountlog',
'AccountV2Handle', 'AccountV2Handle',
'AccountV2Subsystem', 'AccountV2Subsystem',
'ActivityNotFoundError', 'ActivityNotFoundError',
@ -230,6 +231,7 @@ __all__ = [
'apptimer', 'apptimer',
'AppTimer', 'AppTimer',
'asset_loads_allowed', 'asset_loads_allowed',
'atexit',
'balog', 'balog',
'Call', 'Call',
'fullscreen_control_available', 'fullscreen_control_available',
@ -251,6 +253,7 @@ __all__ = [
'DevConsoleTab', 'DevConsoleTab',
'DevConsoleTabEntry', 'DevConsoleTabEntry',
'DevConsoleSubsystem', 'DevConsoleSubsystem',
'DiscordSubsystem',
'DisplayTime', 'DisplayTime',
'displaytime', 'displaytime',
'displaytimer', 'displaytimer',
@ -263,7 +266,7 @@ __all__ = [
'existing', 'existing',
'fade_screen', 'fade_screen',
'fatal_error', 'fatal_error',
'garbage_collect', 'GarbageCollectionSubsystem',
'get_display_resolution', 'get_display_resolution',
'get_immediate_return_code', 'get_immediate_return_code',
'get_input_idle_time', 'get_input_idle_time',
@ -274,6 +277,7 @@ __all__ = [
'get_replays_dir', 'get_replays_dir',
'get_string_height', 'get_string_height',
'get_string_width', 'get_string_width',
'get_suppress_config_and_state_writes',
'get_type_name', 'get_type_name',
'get_ui_scale', 'get_ui_scale',
'get_virtual_safe_area_size', 'get_virtual_safe_area_size',
@ -289,7 +293,7 @@ __all__ = [
'increment_analytics_count', 'increment_analytics_count',
'InputDeviceNotFoundError', 'InputDeviceNotFoundError',
'InputType', 'InputType',
'invoke_main_menu', 'request_main_ui',
'is_browser_likely_available', 'is_browser_likely_available',
'is_browser_likely_available', 'is_browser_likely_available',
'is_os_playing_music', 'is_os_playing_music',
@ -309,6 +313,7 @@ __all__ = [
'mac_music_app_set_volume', 'mac_music_app_set_volume',
'mac_music_app_stop', 'mac_music_app_stop',
'MapNotFoundError', 'MapNotFoundError',
'menu_press',
'MetadataSubsystem', 'MetadataSubsystem',
'music_player_play', 'music_player_play',
'music_player_set_volume', 'music_player_set_volume',
@ -317,6 +322,7 @@ __all__ = [
'native_review_request', 'native_review_request',
'native_review_request_supported', 'native_review_request_supported',
'native_stack_trace', 'native_stack_trace',
'netlog',
'NetworkSubsystem', 'NetworkSubsystem',
'NodeNotFoundError', 'NodeNotFoundError',
'normalized_color', 'normalized_color',
@ -333,10 +339,10 @@ __all__ = [
'PluginSubsystem', 'PluginSubsystem',
'PluginSpec', 'PluginSpec',
'print_load_info', 'print_load_info',
'push_back_press',
'pushcall', 'pushcall',
'quit', 'quit',
'QuitType', 'QuitType',
'reload_hooks',
'reload_media', 'reload_media',
'request_permission', 'request_permission',
'safecolor', 'safecolor',
@ -346,15 +352,16 @@ __all__ = [
'SessionTeamNotFoundError', 'SessionTeamNotFoundError',
'set_analytics_screen', 'set_analytics_screen',
'set_low_level_config_value', 'set_low_level_config_value',
'set_main_ui_input_device',
'set_thread_name', 'set_thread_name',
'set_ui_account_state', 'set_account_sign_in_state',
'set_ui_input_device',
'set_ui_scale', 'set_ui_scale',
'show_progress_bar', 'show_progress_bar',
'shutdown_suppress_begin', 'shutdown_suppress_begin',
'shutdown_suppress_end', 'shutdown_suppress_end',
'shutdown_suppress_count', 'shutdown_suppress_count',
'SimpleSound', 'SimpleSound',
'suppress_config_and_state_writes',
'SpecialChar', 'SpecialChar',
'storagename', 'storagename',
'StringEditAdapter', 'StringEditAdapter',

View file

@ -12,6 +12,8 @@ from typing import TYPE_CHECKING, assert_never
from efro.error import CommunicationError from efro.error import CommunicationError
from efro.call import CallbackSet from efro.call import CallbackSet
from bacommon.login import LoginType from bacommon.login import LoginType
from babase._logging import accountlog
import _babase import _babase
if TYPE_CHECKING: if TYPE_CHECKING:
@ -19,8 +21,6 @@ if TYPE_CHECKING:
from babase._login import LoginAdapter, LoginInfo from babase._login import LoginAdapter, LoginInfo
logger = logging.getLogger('ba.accountv2')
class AccountV2Subsystem: class AccountV2Subsystem:
"""Subsystem for modern account handling in the app. """Subsystem for modern account handling in the app.
@ -104,6 +104,12 @@ class AccountV2Subsystem:
""" """
assert _babase.in_logic_thread() assert _babase.in_logic_thread()
# Inform the base layer of new names/etc.
if account is not None:
_babase.set_account_sign_in_state(True, account.tag)
else:
_babase.set_account_sign_in_state(False)
# Fire any registered callbacks. # Fire any registered callbacks.
for call in self.on_primary_account_changed_callbacks.getcalls(): for call in self.on_primary_account_changed_callbacks.getcalls():
try: try:
@ -280,7 +286,7 @@ class AccountV2Subsystem:
# generally this means the user has explicitly signed in/out or # generally this means the user has explicitly signed in/out or
# switched accounts within that back-end. # switched accounts within that back-end.
if prev_state != new_state: if prev_state != new_state:
logger.debug( accountlog.debug(
'Implicit state changed (%s -> %s);' 'Implicit state changed (%s -> %s);'
' will update app sign-in state accordingly.', ' will update app sign-in state accordingly.',
prev_state, prev_state,
@ -324,7 +330,7 @@ class AccountV2Subsystem:
if self._implicit_signed_in_adapter is None: if self._implicit_signed_in_adapter is None:
# If implicit back-end has signed out, we follow suit # If implicit back-end has signed out, we follow suit
# immediately; no need to wait for network connectivity. # immediately; no need to wait for network connectivity.
logger.debug( accountlog.debug(
'Signing out as result of implicit state change...', 'Signing out as result of implicit state change...',
) )
plus.accounts.set_primary_credentials(None) plus.accounts.set_primary_credentials(None)
@ -343,7 +349,7 @@ class AccountV2Subsystem:
# switching accounts via the back-end). NOTE: should # switching accounts via the back-end). NOTE: should
# test case where we don't have connectivity here. # test case where we don't have connectivity here.
if plus.cloud.is_connected(): if plus.cloud.is_connected():
logger.debug( accountlog.debug(
'Signing in as result of implicit state change...', 'Signing in as result of implicit state change...',
) )
self._implicit_signed_in_adapter.sign_in( self._implicit_signed_in_adapter.sign_in(
@ -352,15 +358,15 @@ class AccountV2Subsystem:
) )
self._implicit_state_changed = False self._implicit_state_changed = False
# Once we've made a move here we don't want to # Once we've made a move here we don't want to do
# do any more automatic stuff. # any more automatic stuff.
self._can_do_auto_sign_in = False self._can_do_auto_sign_in = False
if not self._can_do_auto_sign_in: if not self._can_do_auto_sign_in:
return return
# If we're not currently signed in, we have connectivity, and # If we're not currently signed in, we have connectivity, and we
# we have an available implicit login, auto-sign-in with it once. # have an available implicit login, auto-sign-in with it once.
# The implicit-state-change logic above should keep things # The implicit-state-change logic above should keep things
# mostly in-sync, but that might not always be the case due to # mostly in-sync, but that might not always be the case due to
# connectivity or other issues. We prefer to keep people signed # connectivity or other issues. We prefer to keep people signed
@ -376,7 +382,7 @@ class AccountV2Subsystem:
and not signed_in_v2 and not signed_in_v2
and self._implicit_signed_in_adapter is not None and self._implicit_signed_in_adapter is not None
): ):
logger.debug( accountlog.debug(
'Signing in due to on-launch-auto-sign-in...', 'Signing in due to on-launch-auto-sign-in...',
) )
self._can_do_auto_sign_in = False # Only ATTEMPT once self._can_do_auto_sign_in = False # Only ATTEMPT once
@ -397,11 +403,11 @@ class AccountV2Subsystem:
plus = _babase.app.plus plus = _babase.app.plus
assert plus is not None assert plus is not None
# Make some noise on errors since the user knows a # Make some noise on errors since the user knows a sign-in
# sign-in attempt is happening in this case (the 'explicit' part). # attempt is happening in this case (the 'explicit' part).
if isinstance(result, Exception): if isinstance(result, Exception):
# We expect the occasional communication errors; # We expect the occasional communication errors; Log a full
# Log a full exception for anything else though. # exception for anything else though.
if not isinstance(result, CommunicationError): if not isinstance(result, CommunicationError):
logging.warning( logging.warning(
'Error on explicit accountv2 sign in attempt.', 'Error on explicit accountv2 sign in attempt.',

View file

@ -5,6 +5,7 @@
from __future__ import annotations from __future__ import annotations
import os import os
import time
import logging import logging
from enum import Enum from enum import Enum
from functools import partial from functools import partial
@ -12,8 +13,10 @@ from typing import TYPE_CHECKING, override
from threading import RLock from threading import RLock
from efro.threadpool import ThreadPoolExecutorEx from efro.threadpool import ThreadPoolExecutorEx
from efro.util import strip_exception_tracebacks
import _babase import _babase
from babase._discord import DiscordSubsystem
from babase._language import LanguageSubsystem from babase._language import LanguageSubsystem
from babase._locale import LocaleSubsystem from babase._locale import LocaleSubsystem
from babase._plugin import PluginSubsystem from babase._plugin import PluginSubsystem
@ -27,6 +30,7 @@ from babase._stringedit import StringEditSubsystem
from babase._devconsole import DevConsoleSubsystem from babase._devconsole import DevConsoleSubsystem
from babase._appconfig import AppConfig from babase._appconfig import AppConfig
from babase._logging import lifecyclelog, applog from babase._logging import lifecyclelog, applog
from babase._gc import GarbageCollectionSubsystem
if TYPE_CHECKING: if TYPE_CHECKING:
import asyncio import asyncio
@ -64,6 +68,9 @@ class App:
#: Subsystem for keeping tabs on app health. #: Subsystem for keeping tabs on app health.
health: AppHealthSubsystem health: AppHealthSubsystem
#: Subsystem for network functionality.
net: NetworkSubsystem
#: How long we allow shutdown tasks to run before killing them. #: How long we allow shutdown tasks to run before killing them.
#: Currently the entire app hard-exits if shutdown takes 15 seconds, #: Currently the entire app hard-exits if shutdown takes 15 seconds,
#: so we need to keep it under that. Staying above 10 should allow #: so we need to keep it under that. Staying above 10 should allow
@ -105,6 +112,11 @@ class App:
initializer=self._thread_pool_thread_init, initializer=self._thread_pool_thread_init,
) )
#: Garbage collection related functionality.
self.gc: GarbageCollectionSubsystem = self.register_subsystem(
GarbageCollectionSubsystem()
)
#: Locale related functionality. #: Locale related functionality.
self.locale: LocaleSubsystem = self.register_subsystem( self.locale: LocaleSubsystem = self.register_subsystem(
LocaleSubsystem() LocaleSubsystem()
@ -120,16 +132,18 @@ class App:
PluginSubsystem() PluginSubsystem()
) )
#: Subsystem for discord functionality
self.discord: DiscordSubsystem = self.register_subsystem(
DiscordSubsystem()
)
#: Subsystem for wrangling metadata. #: Subsystem for wrangling metadata.
self.meta: MetadataSubsystem = MetadataSubsystem() self.meta: MetadataSubsystem = MetadataSubsystem()
#: Subsystem for network functionality.
self.net: NetworkSubsystem = NetworkSubsystem()
#: Subsystem for wrangling workspaces. #: Subsystem for wrangling workspaces.
self.workspaces: WorkspaceSubsystem = WorkspaceSubsystem() self.workspaces: WorkspaceSubsystem = WorkspaceSubsystem()
# (not actually in use yet) #: :meta private:
self.components: AppComponentSubsystem = AppComponentSubsystem() self.components: AppComponentSubsystem = AppComponentSubsystem()
#: Subsystem for wrangling text input from various sources. #: Subsystem for wrangling text input from various sources.
@ -242,19 +256,6 @@ class App:
self._asyncio_tasks.add(task) self._asyncio_tasks.add(task)
task.add_done_callback(self._on_task_done) task.add_done_callback(self._on_task_done)
def _on_task_done(self, task: asyncio.Task) -> None:
# Report any errors that occurred.
try:
exc = task.exception()
if exc is not None:
logging.error(
"Error in async task '%s'.", task.get_name(), exc_info=exc
)
except Exception:
logging.exception('Error reporting async task error.')
self._asyncio_tasks.remove(task)
@property @property
def mode_selector(self) -> babase.AppModeSelector: def mode_selector(self) -> babase.AppModeSelector:
"""Controls which app-modes are used for handling given intents. """Controls which app-modes are used for handling given intents.
@ -274,6 +275,24 @@ class App:
def mode_selector(self, selector: babase.AppModeSelector) -> None: def mode_selector(self, selector: babase.AppModeSelector) -> None:
self._mode_selector = selector self._mode_selector = selector
def _on_task_done(self, task: asyncio.Task) -> None:
# Report any errors that occurred.
try:
exc = task.exception()
if exc is not None:
logging.error(
"Error in async task '%s'.", task.get_name(), exc_info=exc
)
# We're done with the exception, so let's rip out its
# tracebacks to try and avoid the need for cyclic
# garbage collection.
strip_exception_tracebacks(exc)
except Exception:
logging.exception('Error reporting async task error.')
self._asyncio_tasks.remove(task)
def _get_subsystem_property( def _get_subsystem_property(
self, ssname: str, create_call: Callable[[], AppSubsystem | None] self, ssname: str, create_call: Callable[[], AppSubsystem | None]
) -> AppSubsystem | None: ) -> AppSubsystem | None:
@ -409,9 +428,16 @@ class App:
def add_shutdown_task(self, coro: Coroutine[None, None, None]) -> None: def add_shutdown_task(self, coro: Coroutine[None, None, None]) -> None:
"""Add a task to be run on app shutdown. """Add a task to be run on app shutdown.
Note that shutdown tasks will be canceled after All shutdown tasks will be run concurrently alongside a fade-out,
:py:const:`SHUTDOWN_TASK_TIMEOUT_SECONDS` if they are still so it is ok for them to take a moment or two to do their thing.
running.
If a shutdown task is still running after
:py:const:`SHUTDOWN_TASK_TIMEOUT_SECONDS`, however, it will be
canceled.
Code needing more exact control over its place in app shutdown
can look into :func:`babase.atexit()`, (though this comes with
some limitations as well).
""" """
if ( if (
self.state is AppState.SHUTTING_DOWN self.state is AppState.SHUTTING_DOWN
@ -423,6 +449,36 @@ class App:
) )
self._shutdown_tasks.append(coro) self._shutdown_tasks.append(coro)
def _pre_interpreter_shutdown(self) -> None:
"""Called just before interpreter is finalized."""
import gc
from babase._env import interpreter_shutdown_sanity_checks
# Spin down connection pools or whatever else used for
# networking.
self.net.pre_interpreter_shutdown()
# Run a last round of cyclic garbage collection - mostly so
# we keep ourselves aware of reference cycles that need cleaning
# up.
self.gc.collect(force=True)
# Turn off any garbage-collector debugging or we'll get a huge
# dump of stuff as Python is tearing itself down, which we don't
# care about.
if gc.get_debug() != 0:
gc.set_debug(0)
# Clear garbage or else we get warnings about uncollectable
# objects if we've been running with gc.DEBUG_SAVEALL.
gc.garbage.clear()
# Finish up anything the threadpool is working on and kill its
# threads.
self.threadpool.shutdown()
# General sanity checks for lingering threads/etc.
interpreter_shutdown_sanity_checks()
def run(self) -> None: def run(self) -> None:
"""Run the app to completion. """Run the app to completion.
@ -746,8 +802,18 @@ class App:
assert _babase.in_logic_thread() assert _babase.in_logic_thread()
# Since we're officially spinning up an app, add some sanity
# checks to help make sure we do a clean exit at the end of it
# (at least on monolithic builds). We add this before we make
# any other on-initing calls that could result in thread
# spinups/etc. so that any of their atexits will have fired
# before this one.
if self.env.monolithic_build:
_babase.atexit(self._pre_interpreter_shutdown)
_env.on_app_state_initing() _env.on_app_state_initing()
self.net = NetworkSubsystem()
self._asyncio_loop = _asyncio.setup_asyncio() self._asyncio_loop = _asyncio.setup_asyncio()
self.health = self.register_subsystem(AppHealthSubsystem()) self.health = self.register_subsystem(AppHealthSubsystem())
@ -870,15 +936,15 @@ class App:
# be added at this point. # be added at this point.
for subsystem in self._subsystems.copy(): for subsystem in self._subsystems.copy():
try: try:
subsystem.do_apply_app_config() subsystem.apply_app_config()
except Exception: except Exception:
logging.exception( logging.exception(
'Error in do_apply_app_config() for subsystem %s.', 'Error in apply_app_config() for subsystem %s.',
subsystem, subsystem,
) )
# Let the native layer do its thing. # Let the native layer do its thing.
_babase.do_apply_app_config() _babase.apply_app_config()
def _update_state(self) -> None: def _update_state(self) -> None:
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
@ -906,6 +972,7 @@ class App:
# Entering suspended state: # Entering suspended state:
if self.state is not AppState.SUSPENDED: if self.state is not AppState.SUSPENDED:
self.state = AppState.SUSPENDED self.state = AppState.SUSPENDED
lifecyclelog.info('app-state is now %s', self.state.name)
self._on_suspend() self._on_suspend()
else: else:
# Leaving suspended state: # Leaving suspended state:
@ -1084,11 +1151,32 @@ class App:
# Kick off a short fade and give it time to complete. # Kick off a short fade and give it time to complete.
lifecyclelog.info('fade-and-shutdown-graphics begin') lifecyclelog.info('fade-and-shutdown-graphics begin')
_babase.fade_screen(False, time=0.15) fade_done = False
await asyncio.sleep(0.15)
# Now tell the graphics system to go down and wait until starttime = time.monotonic()
# it has done so.
def _set_fade_done() -> None:
nonlocal fade_done
fade_done = True
if _babase.app.env.gui:
_babase.fade_screen(False, time=0.25, endcall=_set_fade_done)
else:
fade_done = True
# Note: originally was just sleeping once for the fade duration,
# but due to timing mismatches that could resulted in the game
# freezing visually mid-fade to finish quitting. So now waiting
# until the fade confirms that it is done.
while not fade_done:
await asyncio.sleep(0.03)
# Fallback trigger in case fade never calls back.
if time.monotonic() - starttime > 2.0:
lifecyclelog.warning('fade_screen took too long; cutting off.')
fade_done = True
# Now tell the graphics system to go down and wait until it has
# done so.
_babase.graphics_shutdown_begin() _babase.graphics_shutdown_begin()
while not _babase.graphics_shutdown_is_complete(): while not _babase.graphics_shutdown_is_complete():
await asyncio.sleep(0.01) await asyncio.sleep(0.01)

View file

@ -30,6 +30,10 @@ class AppComponentSubsystem:
Change-callbacks can also be requested for base classes which will Change-callbacks can also be requested for base classes which will
fire in a deferred manner when particular base-classes are fire in a deferred manner when particular base-classes are
overridden. overridden.
(This isn't ready for use yet so hiding it from docs)
:meta private:
""" """
def __init__(self) -> None: def __init__(self) -> None:

View file

@ -49,15 +49,16 @@ class AppConfig(dict):
def default_value(self, key: str) -> Any: def default_value(self, key: str) -> Any:
"""Given a string key, return its predefined default value. """Given a string key, return its predefined default value.
This is the value that will be returned by :meth:`resolve()` This is the value that will be returned by :meth:`resolve()` if
if the key is not present in the config dict or of an incompatible the key is not present in the config dict or of an incompatible
type. type.
Raises an Exception for unrecognized key names. To get the list of keys Raises an Exception for unrecognized key names. To get the list
supported by this method, use babase.AppConfig.builtin_keys(). Note of keys supported by this method, use
that it is perfectly legal to store other data in the config; it just babase.AppConfig.builtin_keys(). Note that it is perfectly legal
needs to be accessed through standard dict methods and missing values to store other data in the config; it just needs to be accessed
handled manually. through standard dict methods and missing values handled
manually.
""" """
return _babase.get_appconfig_default_value(key) return _babase.get_appconfig_default_value(key)
@ -89,8 +90,8 @@ class AppConfig(dict):
def commit(self) -> None: def commit(self) -> None:
"""Commits the config to local storage. """Commits the config to local storage.
Note that this call is asynchronous so the actual write to disk may not Note that this call is asynchronous so the actual write to disk
occur immediately. may not occur immediately.
""" """
commit_app_config() commit_app_config()

View file

@ -56,7 +56,7 @@ class AppSubsystem:
:attr:`~AppState.SHUTDOWN_COMPLETE` state. :attr:`~AppState.SHUTDOWN_COMPLETE` state.
""" """
def do_apply_app_config(self) -> None: def apply_app_config(self) -> None:
"""Called when the app config should be applied.""" """Called when the app config should be applied."""
def on_ui_scale_change(self) -> None: def on_ui_scale_change(self) -> None:

View file

@ -3,9 +3,11 @@
"""Utility functionality related to the overall operation of the app.""" """Utility functionality related to the overall operation of the app."""
from __future__ import annotations from __future__ import annotations
import gc
import os import os
from threading import Thread import json
import time
import asyncio
import threading
from functools import partial from functools import partial
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING, override from typing import TYPE_CHECKING, override
@ -140,6 +142,12 @@ def handle_v1_cloud_log() -> None:
classic.master_server_v1_post('bsLog', info, response) classic.master_server_v1_post('bsLog', info, response)
# TEST
# assert _babase.app.plus is not None
# if _babase.app.plus.cloud.connected:
# print('WILLQUIT')
# _babase.quit()
classic.log_upload_timer_started = True classic.log_upload_timer_started = True
# Delay our log upload slightly in case other pertinent info # Delay our log upload slightly in case other pertinent info
@ -164,14 +172,32 @@ def handle_leftover_v1_cloud_log_file() -> None:
:meta private: :meta private:
""" """
_babase.app.create_async_task(_handle_leftover_v1_cloud_log_file())
async def _handle_leftover_v1_cloud_log_file() -> None:
# Only applies with classic present. # Only applies with classic present.
if _babase.app.classic is None: if _babase.app.classic is None or _babase.app.plus is None:
return return
try:
import json
if os.path.exists(_babase.get_v1_cloud_log_file_path()): if not os.path.exists(_babase.get_v1_cloud_log_file_path()):
return
# Sit and spin until either we've got connectivity or the app is
# shutting down.
while not _babase.app.plus.cloud.connected:
await asyncio.sleep(1.234)
appstate = _babase.app.state
appstate_t = type(appstate)
if (
appstate is appstate_t.SHUTTING_DOWN
or appstate is appstate_t.SHUTDOWN_COMPLETE
):
return
# Ok; it appears we are connected. Let's make one attempt at
# uploading this.
try:
with open( with open(
_babase.get_v1_cloud_log_file_path(), encoding='utf-8' _babase.get_v1_cloud_log_file_path(), encoding='utf-8'
) as infile: ) as infile:
@ -192,50 +218,15 @@ def handle_leftover_v1_cloud_log_file() -> None:
# killed it since. ¯\_(ツ)_/¯ # killed it since. ¯\_(ツ)_/¯
pass pass
_babase.app.classic.master_server_v1_post( _babase.app.classic.master_server_v1_post('bsLog', info, response)
'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(_babase.get_v1_cloud_log_file_path()) os.remove(_babase.get_v1_cloud_log_file_path())
except Exception: except Exception:
balog.exception('Error handling leftover log file.') balog.exception('Error handling leftover log file.')
def garbage_collect_session_end() -> None:
"""Run explicit garbage collection with extra checks for session end."""
gc.collect()
# Can be handy to print this to check for leaks between games.
if bool(False):
print('PY OBJ COUNT', len(gc.get_objects()))
if gc.garbage:
print('PYTHON GC FOUND', len(gc.garbage), 'UNCOLLECTIBLE OBJECTS:')
for i, obj in enumerate(gc.garbage):
print(str(i) + ':', obj)
# NOTE: no longer running these checks. Perhaps we can allow
# running them with an explicit flag passed, but we should never
# run them by default because gc.get_objects() can mess up the app.
# See notes at top of efro.debug.
# if bool(False):
# print_live_object_warnings('after session shutdown')
def garbage_collect() -> None:
"""Run an explicit pass of garbage collection.
May also print warnings/etc. if collection takes too long or if
uncollectible objects are found (so use this instead of simply
:meth:`gc.collect()`.
:meta private:
"""
gc.collect()
def print_corrupt_file_error() -> None: def print_corrupt_file_error() -> None:
"""Print an error if a corrupt file is found.""" """Print an error if a corrupt file is found."""
@ -401,11 +392,23 @@ class AppHealthSubsystem(AppSubsystem):
assert _babase.in_logic_thread() assert _babase.in_logic_thread()
super().__init__() super().__init__()
self._running = True self._running = True
self._thread = Thread(target=self._app_monitor_thread_main, daemon=True)
self._thread.start()
self._response = False self._response = False
self._first_check = True self._first_check = True
self.stop_event = threading.Event()
self.stopped_event = threading.Event()
self._thread = threading.Thread(target=self._app_monitor_thread_main)
self._thread.start()
# Kill our thread as part of app shutdown.
_babase.app.add_shutdown_task(self._shutdown())
async def _shutdown(self) -> None:
self.stop_event.set()
while not self.stopped_event.is_set():
await asyncio.sleep(0.05)
@override @override
def on_app_loading(self) -> None: def on_app_loading(self) -> None:
""":meta private:""" """:meta private:"""
@ -441,15 +444,15 @@ class AppHealthSubsystem(AppSubsystem):
return self._running return self._running
def _monitor_app(self) -> None: def _monitor_app(self) -> None:
import time
while bool(True): while not self.stop_event.is_set():
# Always sleep a bit between checks.
time.sleep(1.234) # # Always sleep a bit between checks.
self.stop_event.wait(1.234)
# Do nothing while backgrounded. # Do nothing while backgrounded.
while not self._running: while not self._running:
time.sleep(2.3456) self.stop_event.wait(2.3456)
# Wait for the logic thread to run something we send it. # Wait for the logic thread to run something we send it.
starttime = time.monotonic() starttime = time.monotonic()
@ -479,6 +482,8 @@ class AppHealthSubsystem(AppSubsystem):
# We just do one alert for now. # We just do one alert for now.
return return
time.sleep(1.042) self.stop_event.wait(1.042)
self._first_check = False self._first_check = False
self.stopped_event.set()

View file

@ -8,7 +8,6 @@ from typing import TYPE_CHECKING, Annotated
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
import threading import threading
import urllib.request
import logging import logging
import weakref import weakref
import time import time
@ -21,9 +20,10 @@ from efro.dataclassio import (
dataclass_from_json, dataclass_from_json,
dataclass_to_json, dataclass_to_json,
) )
import _babase
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any
from bacommon.assets import AssetPackageFlavor from bacommon.assets import AssetPackageFlavor
@ -174,23 +174,28 @@ class AssetGather:
def fetch_url(url: str, filename: Path, asset_gather: AssetGather) -> None: def fetch_url(url: str, filename: Path, asset_gather: AssetGather) -> None:
"""Fetch a given url to a given filename for a given AssetGather.""" """Fetch a given url to a given filename for a given AssetGather."""
# pylint: disable=consider-using-with
import socket import socket
del url # Unused.
# We don't want to keep the provided AssetGather alive, but we want # We don't want to keep the provided AssetGather alive, but we want
# to abort if it dies. # to abort if it dies.
assert isinstance(asset_gather, AssetGather) assert isinstance(asset_gather, AssetGather)
# weak_gather = weakref.ref(asset_gather) # weak_gather = weakref.ref(asset_gather)
if bool(True):
raise RuntimeError('should not be using this')
# Pass a very short timeout to urllib so we have opportunities # Pass a very short timeout to urllib so we have opportunities
# to cancel even with network blockage. # to cancel even with network blockage.
req = urllib.request.urlopen( req: Any = None
urllib.request.Request( # req = urllib.request.urlopen(
url, None, {'User-Agent': _babase.user_agent_string()} # urllib.request.Request(
), # url, None, {'User-Agent': _babase.user_agent_string()}
context=_babase.app.net.sslcontext, # ),
timeout=1, # context=_babase.app.net.sslcontext,
) # timeout=1,
# )
file_size = int(req.headers['Content-Length']) file_size = int(req.headers['Content-Length'])
print(f'\nDownloading: {filename} Bytes: {file_size:,}') print(f'\nDownloading: {filename} Bytes: {file_size:,}')
@ -201,7 +206,7 @@ def fetch_url(url: str, filename: Path, asset_gather: AssetGather) -> None:
# req.close() # req.close()
# req.fp.close() # req.fp.close()
threading.Thread(target=doit).run() threading.Thread(target=doit).start()
with open(filename, 'wb') as outfile: with open(filename, 'wb') as outfile:
file_size_dl = 0 file_size_dl = 0

View file

@ -15,7 +15,11 @@ import logging
import time import time
import os import os
from efro.util import strip_exception_tracebacks
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any
import babase import babase
# Our timer and event loop for the ballistica logic thread. # Our timer and event loop for the ballistica logic thread.
@ -46,6 +50,9 @@ def setup_asyncio() -> asyncio.AbstractEventLoop:
_asyncio_event_loop = asyncio.new_event_loop() _asyncio_event_loop = asyncio.new_event_loop()
_asyncio_event_loop.set_default_executor(babase.app.threadpool) _asyncio_event_loop.set_default_executor(babase.app.threadpool)
# Try to avoid reference loops from exceptions.
_asyncio_event_loop.set_exception_handler(_exception_handler)
# Ideally we should integrate asyncio into our C++ Thread class's # Ideally we should integrate asyncio into our C++ Thread class's
# low level event loop so that asyncio timers/sockets/etc. could # low level event loop so that asyncio timers/sockets/etc. could
# be true first-class citizens. For now, though, we can explicitly # be true first-class citizens. For now, though, we can explicitly
@ -87,3 +94,15 @@ def setup_asyncio() -> asyncio.AbstractEventLoop:
_testtask = _asyncio_event_loop.create_task(aio_test()) _testtask = _asyncio_event_loop.create_task(aio_test())
return _asyncio_event_loop return _asyncio_event_loop
def _exception_handler(
loop: asyncio.AbstractEventLoop, context: dict[str, Any]
) -> None:
# Do default behavior (should log the exception) and then rip out
# exception tracebacks to hopefully avoid reference cycles which
# would require cyclic garbage collection.
loop.default_exception_handler(context)
exc = context.get('exception')
if isinstance(exc, BaseException):
strip_exception_tracebacks(exc)

View file

@ -80,11 +80,12 @@ class DevConsoleTab:
h_align: Literal['left', 'center', 'right'] = 'center', h_align: Literal['left', 'center', 'right'] = 'center',
v_align: Literal['top', 'center', 'bottom', 'none'] = 'center', v_align: Literal['top', 'center', 'bottom', 'none'] = 'center',
scale: float = 1.0, scale: float = 1.0,
style: Literal['normal', 'faded'] = 'normal',
) -> None: ) -> None:
"""Add a button to the tab being refreshed.""" """Add a button to the tab being refreshed."""
assert _babase.app.devconsole.is_refreshing assert _babase.app.devconsole.is_refreshing
_babase.dev_console_add_text( _babase.dev_console_add_text(
text, pos[0], pos[1], h_anchor, h_align, v_align, scale text, pos[0], pos[1], h_anchor, h_align, v_align, scale, style
) )
def python_terminal(self) -> None: def python_terminal(self) -> None:
@ -154,13 +155,19 @@ class DevConsoleSubsystem:
DevConsoleTabEntry('Python', DevConsoleTabPython), DevConsoleTabEntry('Python', DevConsoleTabPython),
DevConsoleTabEntry('AppModes', DevConsoleTabAppModes), DevConsoleTabEntry('AppModes', DevConsoleTabAppModes),
DevConsoleTabEntry('UI', DevConsoleTabUI), DevConsoleTabEntry('UI', DevConsoleTabUI),
DevConsoleTabEntry('Logging', DevConsoleTabLogging), DevConsoleTabEntry('LogLevels', DevConsoleTabLogging),
] ]
if os.environ.get('BA_DEV_CONSOLE_TEST_TAB', '0') == '1': if os.environ.get('BA_DEV_CONSOLE_TEST_TAB', '0') == '1':
self.tabs.append(DevConsoleTabEntry('Test', DevConsoleTabTest)) self.tabs.append(DevConsoleTabEntry('Test', DevConsoleTabTest))
self.is_refreshing = False self.is_refreshing = False
self._tab_instances: dict[str, DevConsoleTab] = {} self._tab_instances: dict[str, DevConsoleTab] = {}
def save_tab(self, tabname: str) -> None:
"""Called by the C++ layer when we should store tab to config."""
cfg = _babase.app.config
cfg['Dev Console Tab'] = tabname
cfg.commit()
def do_refresh_tab(self, tabname: str) -> None: def do_refresh_tab(self, tabname: str) -> None:
"""Called by the C++ layer when a tab should be filled out. """Called by the C++ layer when a tab should be filled out.

View file

@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, override
import _babase import _babase
from babase._logging import description_for_logger
from babase._devconsole import DevConsoleTab from babase._devconsole import DevConsoleTab
if TYPE_CHECKING: if TYPE_CHECKING:
@ -72,7 +73,7 @@ class DevConsoleTabAppModes(DevConsoleTab):
self.text( self.text(
'Available AppModes:', 'Available AppModes:',
scale=0.8, scale=0.8,
pos=(0, 75), pos=(0, 78),
h_align='center', h_align='center',
v_align='center', v_align='center',
) )
@ -80,7 +81,7 @@ class DevConsoleTabAppModes(DevConsoleTab):
for i, mode in enumerate(self._app_modes): for i, mode in enumerate(self._app_modes):
self.button( self.button(
f'{mode.__module__}.{mode.__qualname__}', f'{mode.__module__}.{mode.__qualname__}',
pos=(xoffs + i * bwidth + bpadding, 10), pos=(xoffs + i * bwidth + bpadding, 15),
size=(bwidth - 2.0 * bpadding, 40), size=(bwidth - 2.0 * bpadding, 40),
label_scale=0.6, label_scale=0.6,
call=partial(self._set_app_mode, mode), call=partial(self._set_app_mode, mode),
@ -118,13 +119,14 @@ class DevConsoleTabUI(DevConsoleTab):
def refresh(self) -> None: def refresh(self) -> None:
from babase._mgen.enums import UIScale from babase._mgen.enums import UIScale
xoffs = -375 xoffs = -305
yoffs = 10
self.text( self.text(
'Make sure all UIs either fit in the virtual safe area' 'A UI should either fit in the virtual safe area'
' or dynamically respond to screen size changes.', ' or dynamically respond to screen size changes.',
scale=0.6, scale=0.6,
pos=(xoffs + 15, 70), pos=(xoffs + 15, yoffs + 65),
h_align='left', h_align='left',
v_align='center', v_align='center',
) )
@ -132,7 +134,7 @@ class DevConsoleTabUI(DevConsoleTab):
ui_overlay = _babase.get_draw_virtual_safe_area_bounds() ui_overlay = _babase.get_draw_virtual_safe_area_bounds()
self.button( self.button(
'Virtual Safe Area ON' if ui_overlay else 'Virtual Safe Area OFF', 'Virtual Safe Area ON' if ui_overlay else 'Virtual Safe Area OFF',
pos=(xoffs + 10, 10), pos=(xoffs + 10, yoffs + 10),
size=(200, 30), size=(200, 30),
label_scale=0.6, label_scale=0.6,
call=self.toggle_ui_overlay, call=self.toggle_ui_overlay,
@ -141,7 +143,7 @@ class DevConsoleTabUI(DevConsoleTab):
x = 300 x = 300
self.text( self.text(
'UI-Scale', 'UI-Scale',
pos=(xoffs + x - 5, 15), pos=(xoffs + x - 5, yoffs + 15),
h_align='right', h_align='right',
v_align='none', v_align='none',
scale=0.6, scale=0.6,
@ -151,7 +153,7 @@ class DevConsoleTabUI(DevConsoleTab):
for scale in UIScale: for scale in UIScale:
self.button( self.button(
scale.name.capitalize(), scale.name.capitalize(),
pos=(xoffs + x, 10), pos=(xoffs + x, yoffs + 10),
size=(bwidth, 30), size=(bwidth, 30),
label_scale=0.6, label_scale=0.6,
call=partial(_babase.app.set_ui_scale, scale), call=partial(_babase.app.set_ui_scale, scale),
@ -187,6 +189,7 @@ class Table[T]:
margin_left_right: float = 60.0, margin_left_right: float = 60.0,
debug_bounds: bool = False, debug_bounds: bool = False,
max_columns: int | None = None, max_columns: int | None = None,
focus_entry_config_key: str | None = None,
) -> None: ) -> None:
self._title = title self._title = title
self._entry_width = entry_width self._entry_width = entry_width
@ -198,12 +201,19 @@ class Table[T]:
self._entries = entries self._entries = entries
self._draw_entry_call = draw_entry_call self._draw_entry_call = draw_entry_call
self._max_columns = max_columns self._max_columns = max_columns
self._focus_entry_config_key = focus_entry_config_key
# Values updated on refresh (for aligning other custom # Values updated on refresh (for aligning other custom
# widgets/etc.) # widgets/etc.)
self.top_left: tuple[float, float] = (0.0, 0.0) self.top_left: tuple[float, float] = (0.0, 0.0)
self.top_right: tuple[float, float] = (0.0, 0.0) self.top_right: tuple[float, float] = (0.0, 0.0)
# If we've got a config key, restore any value there.
if self._focus_entry_config_key is not None:
val = _babase.app.config.get(self._focus_entry_config_key)
if isinstance(val, int):
self._focus_entry_index = val
def set_entries(self, entries: list[T]) -> None: def set_entries(self, entries: list[T]) -> None:
"""Update table entries.""" """Update table entries."""
self._entries = entries self._entries = entries
@ -219,6 +229,11 @@ class Table[T]:
This affects which page is shown at the next refresh. This affects which page is shown at the next refresh.
""" """
self._focus_entry_index = max(0, min(len(self._entries) - 1, index)) self._focus_entry_index = max(0, min(len(self._entries) - 1, index))
if self._focus_entry_config_key is not None:
_babase.app.config[self._focus_entry_config_key] = (
self._focus_entry_index
)
_babase.app.config.commit()
def refresh(self, tab: DevConsoleTab) -> None: def refresh(self, tab: DevConsoleTab) -> None:
"""Call to refresh the data.""" """Call to refresh the data."""
@ -386,21 +401,28 @@ class DevConsoleTabLogging(DevConsoleTab):
self._table = Table( self._table = Table(
title='Logging Levels', title='Logging Levels',
entry_width=800, entry_width=800,
entry_height=42, entry_height=44,
debug_bounds=False, debug_bounds=False,
entries=list[str](), entries=list[str](),
draw_entry_call=self._draw_entry, draw_entry_call=self._draw_entry,
max_columns=1, max_columns=1,
focus_entry_config_key='Logging Levels Focus Entry',
) )
@override @override
def refresh(self) -> None: def refresh(self) -> None:
assert self._table is not None assert self._table is not None
# Update table entries with the latest set of loggers (this can # Update table entries with the latest set of loggers (this can
# change over time). # change over time). Sort with 'root' first, followed by all our
# 'ba' loggers, followed by everything else.
self._table.set_entries( self._table.set_entries(
['root'] + sorted(logging.root.manager.loggerDict) ['root']
+ sorted(
logging.root.manager.loggerDict,
key=lambda name: (name.split('.')[0] != 'ba', name),
)
) )
# Draw the table. # Draw the table.
@ -490,13 +512,30 @@ class DevConsoleTabLogging(DevConsoleTab):
xoffs = -15.0 xoffs = -15.0
bwidth = 80.0 bwidth = 80.0
btextscale = 0.5 btextscale = 0.5
if desc := description_for_logger(entry):
tab.text(
desc,
(
# x + width - bwidth * 6.5 - 10.0 + xoffs,
x + 12,
y + height * 0.5 - 12,
),
h_align='left',
scale=0.4,
style='faded',
)
yoffs = 4
else:
yoffs = 0
tab.text( tab.text(
entry, entry,
( (
x + width - bwidth * 6.5 - 10.0 + xoffs, # x + width - bwidth * 6.5 - 10.0 + xoffs,
y + height * 0.5, x + 12,
y + height * 0.5 + yoffs,
), ),
h_align='right', h_align='left',
scale=0.7, scale=0.7,
) )
@ -504,14 +543,6 @@ class DevConsoleTabLogging(DevConsoleTab):
level = logger.level level = logger.level
index = 0 index = 0
effectivelevel = logger.getEffectiveLevel() effectivelevel = logger.getEffectiveLevel()
# if entry != 'root' and level == logging.NOTSET:
# # Show the level being inherited in NOTSET cases.
# notsetlevelname = logging.getLevelName(logger.getEffectiveLevel())
# if notsetlevelname == 'NOTSET':
# notsetname = 'Not Set'
# else:
# notsetname = f'Not Set ({notsetlevelname.capitalize()})'
# else:
notsetname = 'Not Set' notsetname = 'Not Set'
tab.button( tab.button(
notsetname, notsetname,
@ -534,7 +565,6 @@ class DevConsoleTabLogging(DevConsoleTab):
if level == logging.DEBUG if level == logging.DEBUG
else 'blue' if effectivelevel <= logging.DEBUG else 'black' else 'blue' if effectivelevel <= logging.DEBUG else 'black'
), ),
# style='bright' if level == logging.DEBUG else 'normal',
call=partial( call=partial(
self._set_entry_val, entry_index, entry, logging.DEBUG self._set_entry_val, entry_index, entry, logging.DEBUG
), ),
@ -550,7 +580,6 @@ class DevConsoleTabLogging(DevConsoleTab):
if level == logging.INFO if level == logging.INFO
else 'white' if effectivelevel <= logging.INFO else 'black' else 'white' if effectivelevel <= logging.INFO else 'black'
), ),
# style='bright' if level == logging.INFO else 'normal',
call=partial(self._set_entry_val, entry_index, entry, logging.INFO), call=partial(self._set_entry_val, entry_index, entry, logging.INFO),
) )
index += 1 index += 1

121
dist/ba_data/python/babase/_discord.py vendored Normal file
View file

@ -0,0 +1,121 @@
# Released under the MIT License. See LICENSE for details.
"""Functionality related to discord sdk integration"""
from __future__ import annotations
from typing import TYPE_CHECKING, override
import _babase
from babase._appsubsystem import AppSubsystem
if TYPE_CHECKING:
from typing import Any
# Add a config key preferably for this
ENABLE_DISCORD = True # disable this for now
class DiscordSubsystem(AppSubsystem):
"""Discord SDK integration class.
Access the single shared instance of this class via the
:attr:`~babase.App.discord` attr on the :class:`~babase.App` class."""
# pylint: disable=too-many-positional-arguments
def __init__(self) -> None:
self.details: str | None = None
self.state: str | None = None
self.large_image_key: str | None = None
self.small_image_key: str | None = None
self.large_image_text: str | None = None
self.small_image_text: str | None = None
self.start_timestamp: float | None = None
self.end_timestamp: float | None = None
if not ENABLE_DISCORD:
return
if not self.is_available():
return
_babase.discord_start()
@override
def on_app_shutdown(self) -> None:
"""Called when the app is shutting down."""
_babase.discord_shutdown()
@staticmethod
def is_available() -> bool:
"""Check if the Discord SDK is available.
_babase.discord_is_ready() returns None if not available."""
return _babase.discord_is_ready() is not None
@property
def is_ready(self) -> bool:
"""Check if the Discord SDK is ready."""
return _babase.discord_is_ready()
def set_presence(
self,
state: str | None = None,
details: str | None = None,
start_timestamp: float | None = None,
end_timestamp: float | None = None,
large_image_key: str | None = None,
small_image_key: str | None = None,
large_image_text: str | None = None,
small_image_text: str | None = None,
party_id: str | None = None,
party_size: tuple[int, int] | None = None,
) -> None:
"""Set Discord rich presence state.
Args:
state: Current game state (e.g. "In Match", "Main Menu")
details: Additional details about current activity
start_timestamp: Activity start time (epoch timestamp)
end_timestamp: Activity end time (epoch timestamp)
large_image_key: Key/Url for large image asset
large_image_text: Hover text for large image
small_image_key: Key/Url for small image asset
small_image_text: Hover text for small image
party_id: Current party ID for join/spectate
party_size: Tuple of (current_size, max_size)
"""
if not self.is_available():
return
# Build presence dict with only non-None values
presence: dict[str, Any] = {}
if state is not None:
self.state = state
presence['state'] = state
if details is not None:
self.details = details
presence['details'] = details
if start_timestamp is not None:
self.start_timestamp = start_timestamp
presence['start_timestamp'] = start_timestamp
if end_timestamp is not None:
self.end_timestamp = end_timestamp
presence['end_timestamp'] = end_timestamp
if large_image_key is not None:
self.large_image_key = large_image_key
presence['large_image_key'] = large_image_key
if small_image_key is not None:
self.small_image_key = small_image_key
presence['small_image_key'] = small_image_key
if large_image_text is not None:
self.large_image_text = large_image_text
presence['large_image_text'] = large_image_text
if small_image_text is not None:
self.small_image_text = small_image_text
presence['small_image_text'] = small_image_text
# Set party info if provided
if party_id is not None:
_babase.discord_set_party(party_id=party_id)
if party_size is not None:
_babase.discord_set_party(
current_party_size=party_size[0], max_party_size=party_size[1]
)
# Update rich presence
if presence:
_babase.discord_richpresence(**presence)

View file

@ -3,20 +3,34 @@
"""Environment related functionality.""" """Environment related functionality."""
from __future__ import annotations from __future__ import annotations
import os
import sys import sys
import ssl
import time
import signal import signal
import logging import logging
import warnings import warnings
import threading
from typing import TYPE_CHECKING, override from typing import TYPE_CHECKING, override
import urllib3
from efro.logging import LogLevel from efro.logging import LogLevel
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any from typing import Any
from efro.logging import LogEntry, LogHandler from efro.logging import LogEntry, LogHandler
_g_babase_imported = False # pylint: disable=invalid-name # Timeout for standard functions talking to the master-server/etc. We
_g_babase_app_started = False # pylint: disable=invalid-name # generally try to fail fast and retry instead of waiting a long time
# for things.
DEFAULT_REQUEST_TIMEOUT_SECONDS = 10
_g_babase_imported: bool = False
_g_babase_app_started: bool = False
_g_net_warm_start_thread: threading.Thread | None = None
_g_net_warm_start_ssl_context: ssl.SSLContext | None = None
_g_net_warm_start_pool_manager: urllib3.PoolManager | None = None
def on_native_module_import() -> None: def on_native_module_import() -> None:
@ -26,6 +40,7 @@ def on_native_module_import() -> None:
environment modifications until we actually commit to running an environment modifications until we actually commit to running an
app. app.
""" """
# pylint: disable=cyclic-import
import _babase import _babase
import baenv import baenv
@ -36,7 +51,7 @@ def on_native_module_import() -> None:
# If we have a log_handler set up, wire it up to feed _babase its # If we have a log_handler set up, wire it up to feed _babase its
# output. # output.
envconfig = baenv.get_config() envconfig = baenv.get_env_config()
if envconfig.log_handler is not None: if envconfig.log_handler is not None:
_feed_logs_to_babase(envconfig.log_handler) _feed_logs_to_babase(envconfig.log_handler)
@ -45,22 +60,24 @@ def on_native_module_import() -> None:
lambda: _babase.set_thread_name('ballistica logging') lambda: _babase.set_thread_name('ballistica logging')
) )
env = _babase.pre_env() pre_env = _babase.pre_env()
# Give a soft warning if we're being used with a different binary # Give a soft warning if we're being used with a different binary
# version than we were built for. # version than we were built for.
running_build: int = env['build_number'] running_build: int = pre_env['build_number']
assert isinstance(running_build, int)
if running_build != baenv.TARGET_BALLISTICA_BUILD: if running_build != baenv.TARGET_BALLISTICA_BUILD:
logging.warning( logging.error(
'These scripts are meant to be used with' 'These scripts are meant to be used with'
' Ballistica build %d, but you are running build %d.' ' Ballistica build %d, but you are running build %d.'
" This might cause problems. Module path: '%s'.", " This is likely to cause problems. Module path: '%s'.",
baenv.TARGET_BALLISTICA_BUILD, baenv.TARGET_BALLISTICA_BUILD,
running_build, running_build,
__file__, __file__,
) )
debug_build = env['debug_build'] debug_build = pre_env['debug_build']
# We expect dev_mode on in debug builds and off otherwise; # We expect dev_mode on in debug builds and off otherwise;
# make noise if that's not the case. # make noise if that's not the case.
@ -82,6 +99,7 @@ def on_main_thread_start_app() -> None:
as we like it for running our app stuff. This includes things like as we like it for running our app stuff. This includes things like
signal-handling, garbage-collection, and logging. signal-handling, garbage-collection, and logging.
""" """
# pylint: disable=cyclic-import
import gc import gc
import baenv import baenv
import _babase import _babase
@ -91,7 +109,7 @@ def on_main_thread_start_app() -> None:
_g_babase_app_started = True _g_babase_app_started = True
assert _g_babase_imported assert _g_babase_imported
assert baenv.config_exists() assert baenv.env_config_exists()
# If we were unable to set paths earlier, complain now. # If we were unable to set paths earlier, complain now.
if baenv.did_paths_set_fail(): if baenv.did_paths_set_fail():
@ -115,24 +133,20 @@ def on_main_thread_start_app() -> None:
# release builds so its good to have this on everywhere. # release builds so its good to have this on everywhere.
warnings.simplefilter('default', DeprecationWarning) warnings.simplefilter('default', DeprecationWarning)
# Turn off fancy-pants cyclic garbage-collection. We run it only at # Set up our garbage collection stuff.
# explicit times to avoid random hitches and keep things more _babase.app.gc.set_initial_mode()
# deterministic. Non-reference-looped objects will still get cleaned
# up immediately, so we should try to structure things to avoid
# reference loops (just like Swift, ObjC, etc).
# FIXME - move this to Python bootstrapping code. or perhaps disable if os.environ.get('BA_GC_DEBUG_LEAK') == '1':
# it completely since we've got more bg stuff happening now?... print('ENABLING GC DEBUG LEAK CHECKS', file=sys.stderr)
# (but put safeguards in place to time/minimize gc pauses). gc.set_debug(gc.DEBUG_LEAK)
gc.disable()
# pylint: disable=c-extension-no-member # pylint: disable=c-extension-no-member
if not TYPE_CHECKING: if not TYPE_CHECKING:
import __main__ import __main__
# Clear out the standard quit/exit messages since they don't # Clear out the standard quit/exit messages since they don't
# work in our embedded situation (should revisit this once we're # work in our embedded situations and we wouldn't want to use them
# usable from a standard interpreter). Note that these don't # if they did since Note that these don't
# exist in the first place for our monolithic builds which don't # exist in the first place for our monolithic builds which don't
# use site.py. # use site.py.
for attr in ('quit', 'exit'): for attr in ('quit', 'exit'):
@ -164,6 +178,334 @@ def on_main_thread_start_app() -> None:
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
# Kick off networking bootstrapping. We do this here instead of in
# our app net-subsystem so that it can proceed in parallel with the
# rest of our bootstrapping (as networking stuff is often an overall
# bottleneck). Our net-subsystem then pulls this stuff into itself
# when it comes up.
global _g_net_warm_start_thread # pylint: disable=global-statement
_g_net_warm_start_thread = threading.Thread(target=_bootstrap_networking)
_g_net_warm_start_thread.start()
# Kick off some background cache cleanup operations.
threading.Thread(target=_pycache_upkeep).start()
def _bootstrap_networking() -> None:
"""Early networking bootstrapping.
We start this as early as we can after start-app is called to
give it as much time to warm-start network connections/etc. while
we're spinning up other stuff.
"""
from efro.util import strict_partial
import _babase
from babase._logging import netlog
netlog.debug('_bootstrap_networking() begin')
# Our shared SSL context. Creating these can be expensive so we
# create it here once and recycle for our various connections.
global _g_net_warm_start_ssl_context # pylint: disable=global-statement
_g_net_warm_start_ssl_context = ssl.create_default_context()
# I'm finding that urllib3 exceptions tend to give us reference
# cycles, which we want to avoid as much as possible. We can work
# around this by gutting the exceptions using
# efro.util.strip_exception_tracebacks() after handling them.
# Unfortunately this means we need to turn off retries here since
# the retry mechanism effectively hides exceptions from us.
global _g_net_warm_start_pool_manager # pylint: disable=global-statement
_g_net_warm_start_pool_manager = urllib3.PoolManager(
retries=False,
ssl_context=_g_net_warm_start_ssl_context,
timeout=urllib3.util.Timeout(total=DEFAULT_REQUEST_TIMEOUT_SECONDS),
maxsize=10,
headers={'User-Agent': _babase.user_agent_string()},
)
# Kick off a request to our first-choice bootstrap server. This can
# get dns lookup and ssl negotiation and whatnot going in the
# background while the rest of our app is coming up, and our first
# actual request to the server will ideally have an established
# connection already waiting for it.
#
# We kick off a separate thread to handle this though since we don't
# want the bootstrap process to block on it if its still going when
# our bootstrapping results are needed.
threading.Thread(
target=strict_partial(
_warm_start_bootstrap_connection, _g_net_warm_start_pool_manager
)
).start()
netlog.debug('_bootstrap_networking() end')
def _warm_start_bootstrap_connection(pool: urllib3.PoolManager) -> None:
from efro.util import strip_exception_tracebacks
from babase._logging import netlog
import _babase
starttime = time.monotonic()
try:
netlog.debug('Warm starting urllib3 pool...')
# Slightly hacky: this runs early enough that the plus subsystem
# doesn't exist yet so we need to hard-code this address (the
# first bootstrap address that Plus returns). Note that we don't
# actually *use* the results of this request; we're just getting
# urllib3 to establish a connection to our first-choice server
# to hopefully have it available for immediate use when needed.
response = pool.request('GET', 'https://regional.ballistica.net/ping')
_data = response.data
netlog.debug(
'Warm starting urllib3 pool succeeded in %.3fs.',
time.monotonic() - starttime,
)
except Exception as exc:
netlog.debug(
'Warm starting urllib3 pool failed in %.3fs.',
time.monotonic() - starttime,
exc_info=True,
)
# Hopefully avoid reference cycles.
strip_exception_tracebacks(exc)
def _pycache_upkeep() -> None:
from babase._logging import cachelog
try:
_do_pycache_upkeep()
except Exception:
cachelog.exception('Error in pycache upkeep.')
def _do_pycache_upkeep() -> None:
"""Take a quick pass at generating pycs for all .py files."""
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
import py_compile
import importlib.util
from efro.util import prune_empty_dirs
import _babase
from babase._logging import cachelog
# Skip this all if bytecode writing is disabled.
if sys.dont_write_bytecode:
return
def should_abort() -> bool:
appstate = _babase.app.state
appstate_t = type(appstate)
return (
appstate is appstate_t.SHUTTING_DOWN
or appstate is appstate_t.SHUTDOWN_COMPLETE
)
# Let's wait until the app has been in the running state for a wee
# bit before doing our thing; that way we're out of the way of more
# high priority stuff like meta-scans that happen at first launch.
# Packing too much work in at once is likely to lead to visible
# stutters and ANR issues and whatnot.
time_in_running_state = 0.0
sleep_inc = 0.1
while time_in_running_state < 10.0:
time.sleep(sleep_inc)
appstate = _babase.app.state
appstate_t = type(appstate)
if appstate is appstate_t.RUNNING:
time_in_running_state += sleep_inc
if should_abort():
cachelog.debug('Aborting pycache update early due to app shutdown.')
return
cachelog.info('Running pycache upkeep...')
# Measure time from when we actually start working.
starttime = time.monotonic()
env = _babase.app.env
stdlibpath = os.path.dirname(py_compile.__file__)
srcdirs: list[str | None] = [
env.python_directory_app,
env.python_directory_app_site,
stdlibpath,
env.python_directory_user,
]
# Skip over particular dirnames; namely stuff in stdlib we're very
# unlikely to ever use. This shaves off quite a bit of work.
skip_dirs = {
'test',
'email',
'__pycache__',
'idlelib',
'tkinter',
'turtledemo',
'unittest',
'encodings',
}
# We do lots of stuff and if everything spits an error it's gonna
# get messy, so let's only warn on the first thing that goes wrong
# (the rest can be debug messages).
complained = False
def complain(msg: str) -> None:
nonlocal complained
if complained:
cachelog.debug('(repeat) Error updating pycache dir: %s', msg)
return
cachelog.warning('Error updating pycache dir: %s', msg)
# Build a dict of dst pyc paths mapped to src py paths and
# src py modtimes.
entries: dict[str, tuple[str, float]] = {}
for srcdir in srcdirs:
if srcdir is None or not os.path.isdir(srcdir):
continue
for dpath, dnames, fnames in os.walk(srcdir):
# Modify dirnames in-place to prevent walk from descending
# into them.
dnames[:] = [d for d in dnames if d not in skip_dirs]
for fname in fnames:
if not fname.endswith('.py'):
continue
srcpath = os.path.join(dpath, fname)
dstpath = importlib.util.cache_from_source(srcpath)
srcmodtime = os.path.getmtime(srcpath)
entries[dstpath] = (srcpath, srcmodtime)
if should_abort():
cachelog.debug(
'Aborting pycache update early due to app shutdown.'
)
return
pycdir = os.path.join(env.cache_directory, 'pyc')
# Sanity test: make sure these pyc paths appear to be under our
# designated cache dir.
for entry in entries:
if not entry.startswith(pycdir):
complain(
f'pyc target {entry}'
f' does not start with expected prefix {pycdir}.'
)
# Just check the first.
break
def _has_py_source(path: str) -> bool:
"""Does this .pyc path have an associated existing .py file?"""
if not path.endswith('.pyc'):
return False
try:
srcpath = importlib.util.source_from_cache(fullpath)
except Exception as exc:
# Have gotten reports of failures here on a file named
# hook-mitmproxy.addons.onboardingapp.cpython-313.pyc'
# (found in site-packages on a linux install).
if 'expected only 2 or 3 dots' in str(exc):
pass
else:
complain(f'Error looking for py src for "{path}": {exc}')
# If anything goes wrong, just assume it *does* have a source;
# let's only kill stuff when we're sure it doesn't.
return True
return os.path.exists(srcpath)
def _is_older_than_a_few_seconds(path: str) -> bool:
try:
return os.path.getmtime(path) < time.time() - 10
except FileNotFoundError:
# Transient files such as in-progress pycache temp files are
# likely to disappear under us. Just consider that as 'not
# old'.
return False
# Now kill all files in our dst pyc dir that *don't* appear in our
# dict of dst paths.
for dpath, dnames, fnames in os.walk(pycdir):
for fname in fnames:
fullpath = os.path.join(dpath, fname)
# We excluded skip_dirs when we generated entries, but its
# still possible that stuff from those dirs has been cached
# on-demand. So to be extra sure we can delete something we
# make sure it isn't a .pyc file with an existing src .py
# file. Technically we could check *everything* this way but
# it should be lots faster to fast-out with the entries dict
# lookup first.
#
# We also now check to make sure files are older than a few
# seconds before deleting them; this keeps us out of the way
# of in-progress .pyc temp files.
if (
fullpath not in entries
and _is_older_than_a_few_seconds(fullpath)
and not _has_py_source(fullpath)
):
try:
cachelog.debug(
'pycache-upkeep: pruning file \'%s\'.', fullpath
)
os.unlink(fullpath)
except Exception as exc:
complain(f'Failed to delete file "{fullpath}": {exc}')
# Ok, we've killed all files that aren't valid cache files. Now
# prune all empty dirs.
try:
prune_empty_dirs(pycdir)
except Exception as exc:
complain(str(exc))
# Lastly, go through all src paths and compile all dst paths that
# don't exist or are outdated.
for dstpath, (srcpath, srcmtime) in entries.items():
if not os.path.exists(dstpath) or srcmtime > os.path.getmtime(dstpath):
try:
cachelog.debug('pycache-upkeep: precompiling \'%s\'.', srcpath)
py_compile.compile(srcpath, doraise=True)
# Sleep a bit to limit speed to roughly 100/second max.
# Hopefully that will reduce any stuttering effects from
# this and it should still take only a few seconds on
# fast hardware.
time.sleep(0.01)
except Exception as exc:
# The first time a compile fails, let's pause and see if
# it actually wound up updated first. There's a chance
# we could hit the odd sporadic issue trying to update a
# file that Python is already updating.
if not complained:
time.sleep(0.2)
still_out_of_date = not os.path.exists(
dstpath
) or srcmtime > os.path.getmtime(dstpath)
if still_out_of_date:
complain(f'Error precompiling {fullpath}: {exc}')
assert complained
if should_abort():
cachelog.debug(
'Aborting pycache update early due to app shutdown.'
)
return
duration = time.monotonic() - starttime
cachelog.info('Pycache upkeep completed in %.3fs.', duration)
def on_app_state_initing() -> None: def on_app_state_initing() -> None:
"""Called when the app reaches the initing state.""" """Called when the app reaches the initing state."""
@ -174,7 +516,7 @@ def on_app_state_initing() -> None:
# Let the user know if the app Python dir is a 'user' one. This is a # Let the user know if the app Python dir is a 'user' one. This is a
# risky thing to be doing so don't let them forget they're doing it. # risky thing to be doing so don't let them forget they're doing it.
envconfig = baenv.get_config() envconfig = baenv.get_env_config()
if envconfig.is_user_app_python_dir: if envconfig.is_user_app_python_dir:
_babase.screenmessage( _babase.screenmessage(
f"Using user system scripts: '{envconfig.app_python_dir}'", f"Using user system scripts: '{envconfig.app_python_dir}'",
@ -182,6 +524,56 @@ def on_app_state_initing() -> None:
) )
def interpreter_shutdown_sanity_checks() -> None:
"""Run sanity checks just before finalizing after an app run."""
import baenv
from babase._logging import applog
env_config = baenv.get_env_config()
main_thread = threading.main_thread()
# Warn about any still-running threads that we don't expect to find.
warn_threads: list[threading.Thread] = []
for thread in threading.enumerate():
if thread is main_thread:
continue
# Our log-handler thread gets set up early and will get torn
# down after us; we expect it to still be around.
if (
env_config.log_handler is not None
and thread
is env_config.log_handler._thread # pylint: disable=W0212
):
continue
# Dummy threads are native threads that happen to have Python
# stuff called in them. Ideally we should be using all Python
# threads so should clear these out at some point. Just ignoring
# them for now though.
#
if isinstance(thread, threading._DummyThread): # pylint: disable=W0212
continue
warn_threads.append(thread)
if warn_threads:
applog.warning(
'%s',
'\n '.join(
[
f'{len(warn_threads)}'
f' unexpected thread(s) still running at'
f' Python shutdown:'
]
+ [str(t) for t in warn_threads]
)
+ '\nThreads should spin themselves down at app shutdown'
' (see App.add_shutdown_task()).',
)
def _feed_logs_to_babase(log_handler: LogHandler) -> None: def _feed_logs_to_babase(log_handler: LogHandler) -> None:
"""Route log/print output to internal ballistica console/etc.""" """Route log/print output to internal ballistica console/etc."""
import _babase import _babase

564
dist/ba_data/python/babase/_gc.py vendored Normal file
View file

@ -0,0 +1,564 @@
# Released under the MIT License. See LICENSE for details.
#
"""Utility functionality related to the overall operation of the app."""
from __future__ import annotations
import gc
import os
import time
import random
import logging
from enum import Enum
from typing import TYPE_CHECKING, assert_never, override
import bacommon.logging
import _babase
from babase._appsubsystem import AppSubsystem
from babase._logging import gc_log
if TYPE_CHECKING:
import datetime
from typing import Any, TextIO, Callable
import babase
class GarbageCollectionSubsystem(AppSubsystem):
"""Garbage collection functionality for the app.
Access the single shared instance of this class via the
:attr:`~babase.App.gc` attr on the :class:`~babase.App` class.
Design
======
Python objects are deallocated in one of two ways: either they are
immediately deallocated when the last reference to them disappears,
or they are later deallocated by the cyclic garbage collector, which
looks for groups of objects retaining references to each other but
otherwise unaccessible and deallocates them as a group (see:
Python's :mod:`gc` module).
Python's garbage collector runs at arbitrary times and can be
expensive to run, making it liable to cause visual hitches in game
situations such as ours. For this reason, Ballistica disables it by
default and instead runs explicit passes at times when hitches won't
be noticable; namely during in-game transitions when the screen is
faded to black.
Because significant time can pass between these explicit passes, we
try to minimize the number of objects relying on the garbage
collector for cleanup; otherwise we risk bloating memory usage if a
single uninterrupted stretch of gameplay repeatedly generates such
objects. It is generally desirable to avoid reference cycles anyway
and keep deallocation of objects deterministic and predictable.
To aid in minimizing garbage-collector reliance, Ballistica's
standard behavior is to flip on some Python garbage-collection debug
flags, examine objects being garbage-collected, and provide the user
with information and warnings if the number of such objects gets too
large. Controls are provided here to adjust that behavior (see
:attr:`mode`).
Note that the goal is simply to keep garbage collection under
control; not to eliminate it completely. There seems to be a number
of situations, even within Python stdlib modules, where reference
loops are mostly unavoidable, and trying to hunt down every last one
seems like an exercise in futility. We mostly just aim to keep
things under our warning thresholds so runaway memory usage never
becomes a problem.
Usage
=====
To switch garbage-collection modes for debugging, do something like::
babase.app.gc.mode = babase.app.gc.Mode.LEAK_DEBUG
The engine will remember modes you set this way, allowing you to set
a mode and then debug repeated runs of the app. Just remember to
switch back to :attr:`~Mode.STANDARD` mode when finished.
You can also set mode using the environment variable
``BA_GC_MODE``. Modes set this way take precedence over
modes set using the above method and only apply for the current run.
For example, to run in :attr:`~Mode.LEAK_DEBUG` mode:
.. code-block:: sh
BA_GC_MODE=leak_debug ./bombsquad
"""
class Mode(Enum):
"""Garbage-collection modes the app can be in.
For most of these modes, the engine will assume control of
Python's garbage collector - disabling automatic collection,
setting debug flags, and running explicit collections on the
engine's behalf (You can set :attr:`DISABLED` mode if you need
to avoid this).
"""
#: In this mode, when the engine runs an explicit garbage
#: collection pass, it examine the results and logs basic useful
#: info such as total number of collected objects by type to the
#: :attr:`~bacommon.logging.ClientLoggerName.GARBAGE_COLLECTION`
#: logger. By default these will be :obj:`~logging.INFO` or
#: :obj:`~logging.DEBUG` log messages (generally not visible by
#: default), but if ever too many objects are handled in a
#: single pass, a single :obj:`~logging.WARNING` will be emitted
#: instead.
#:
#: The general idea is that this mode stays out of your way
#: during normal app operation but warns you if things ever get
#: messy enough to need attention. You can then use the basic
#: info provided by its log messages to address the issue or, if
#: need be, you can flip to :attr:`LEAK_DEBUG` mode to dive in
#: deeper.
STANDARD = 'standard'
#: In this mode, Python's garbage-collector is set to the
#: :obj:`gc.DEBUG_LEAK` flag, which causes information on all
#: objects handled by the garbage-collector to be printed and
#: all collected objects to be stored in :obj:`gc.garbage`. Be
#: aware that in this mode *NOTHING* is actually deallocated
#: by the garbage-collector, so only use this for debugging.
#: This mode is useful for interactively digging into particular
#: reference cycles; you can use :meth:`efro.debug.getobj()` to
#: find an object based on the hex id printed for it and then
#: use :meth:`efro.debug.printrefs()` to look into what is
#: referencing that object.
#:
#: Example::
#:
#: # Output from a garbage collection pass in LEAK_DEBUG mode,
#: # listing objects the garbage-collector handled:
#: # gc: collectable <tuple 0x105f95860>
#: # gc: collectable <type 0x12e059030>
#: # gc: collectable <tuple 0x1060f1400>
#: # gc: collectable <getset_descriptor 0x106234470>
#: # gc: collectable <dict 0x1062342f0>
#: # gc: collectable <getset_descriptor 0x1062344d0>
#:
#: # We can then use printrefs() to see what is referencing some
#: # object, what is referencing those references, etc.
#: from efro.debug import printrefs, getobj
#: printrefs(getobj(0x1060f1400))
#:
#: # Output:
#: # tuple @ 0x1060f1400 (len 2, contains [type, type])
#: # type @ 0x12e059030
#: # tuple @ 0x1060f1400 (len 2, contains [type, type])
#: # getset_descriptor @ 0x106234470
#: # getset_descriptor @ 0x1062344d0
LEAK_DEBUG = 'leak_debug'
#: In this mode, Python's garbage collection is left completely
#: untouched. Use this if you want to do some sort of manual
#: debugging/experimenting where our default logic would get in
#: the way. Note that you should generally restart the app after
#: switching to this mode, as it will not undo any changes that
#: have already been made by other modes.
DISABLED = 'disabled'
_MODE_CONFIG_KEY = 'Garbage Collection Mode'
_SCREEN_MSG_COLOR = (1.0, 0.8, 0.4)
def __init__(self) -> None:
#: A :func:`time.monotonic()` value updated whenever we do an
#: actual :func:`gc.collect()`. Note that not all calls to our
#: :meth:`collect()` method result in an actual
#: :func:`gc.collect()` happening (if not enough time has
#: passed, etc.)
self.last_actual_collect_time: float | None = None
self._total_num_gc_objects = 0
self._last_collection_time: float | None = None
self._showed_standard_mode_warning = False
self._mode: GarbageCollectionSubsystem.Mode | None = None
@override
def on_app_running(self) -> None:
""":meta private:"""
# Inform the user if we're set to something besides standard
# (so they don't forget to switch it back when done).
if self._mode is not None and self._mode is not self.Mode.STANDARD:
_babase.screenmessage(
f'Garbage-gollection mode is {self._mode.name}.',
color=self._SCREEN_MSG_COLOR,
)
# Also log some usage tips (handy to copy/paste).
if self._mode is self.Mode.LEAK_DEBUG:
gc_log.warning(
(
'Garbage-gollection mode is %s.\n'
'Eliminate ref-loops to minimize'
' garbage-collected objects.\n'
'Set %s logger to INFO or DEBUG for more info.\n'
'To debug refs for an object, do:'
' `from efro.debug import printrefs, getobj;'
' printrefs(getobj(OBJID))`.'
),
self._mode.name,
bacommon.logging.ClientLoggerName.GARBAGE_COLLECTION.value,
)
@property
def mode(self) -> Mode:
"""The app's current garbage-collection mode.
Be aware that changes to this mode persist across runs of the
app (you will see on-screen warnings at launch if it is set to
non-default value).
"""
if self._mode is None:
raise RuntimeError('Initial mode has not yet been set.')
return self._mode
@mode.setter
def mode(self, mode: Mode) -> None:
cls = type(mode)
_babase.screenmessage(
f'Garbage-gollection mode is now {mode.name}.',
color=self._SCREEN_MSG_COLOR,
)
self._apply_mode(mode)
# Store to app config.
cfg = _babase.app.config
if mode is cls.STANDARD:
# For default, store nothing.
cfg.pop(self._MODE_CONFIG_KEY, None)
else:
cfg[self._MODE_CONFIG_KEY] = mode.value
cfg.commit()
def collect(self, force: bool = False) -> None:
"""Request an explicit garbage collection pass.
Apps should call this when visual hitches would not be noticed,
such as when the screen is faded to black during transitions.
The effect of this call is influenced by the current
:attr:`mode` and other factors. For instance, if mode is
:attr:`Mode.DISABLED` or if not enough time has passed since the
last collect, then this call is a no-op.
"""
if self._mode is None:
gc_log.debug(
'Skipping explicit gc pass (no mode set).',
)
return
if self._mode is self.Mode.DISABLED:
gc_log.debug(
'Skipping explicit gc pass (mode is %s).',
self.Mode.DISABLED.name,
)
return
# If we find automatic gc is somehow enabled, abort.
if gc.isenabled():
gc_log.debug(
'Skipping explicit gc pass'
' (automatic collection is enabled).'
)
return
# Even when nothing is collected, a full gc pass is a bit of
# work, so skip runs if they happen too close together.
now = time.monotonic()
if (
self._last_collection_time is not None
and now - self._last_collection_time < 20
and not force
):
gc_log.debug('Skipping explicit gc pass (too little time passed).')
return
# Also let's skip occasional runs randomly to shake things up a
# bit for object cleanup checks. For example, if we're running a
# check a few seconds after a game ends to make sure all game
# objects have been deallocated, an explicit GC pass that
# consistently happens around that same time could hide
# reference loops that we'd like to know about. If we skip the
# GC occasionally, those sorts of issues are more likely to come
# to light.
if not force and random.random() > 0.8:
gc_log.debug('Skipping explicit gc pass (random jitter).')
return
if self._mode is self.Mode.STANDARD:
self._collect_standard(now)
elif self._mode is self.Mode.LEAK_DEBUG:
self._collect_leak_debug(now)
else:
assert_never(self._mode)
self._last_collection_time = now
def set_initial_mode(self) -> None:
""":meta private:"""
# If an env var is set, that takes priority.
envval = os.environ.get('BA_GC_MODE')
if envval:
try:
self._mode = self.Mode(envval)
except ValueError:
gc_log.warning(
'Invalid garbage-collection-mode; valid options are %s.',
[m.value for m in self.Mode],
)
if self._mode is None:
self._mode = self._mode_from_config()
self._apply_mode(self._mode)
def _collect_standard(self, now: float) -> None:
assert gc.get_debug() == gc.DEBUG_SAVEALL
# Make more noise (warning instead of info) if there's a
# substantial number of collections in a single cycle.
gc_threshold = 50
starttime = now
num_affected_objs = gc.collect()
now2 = self.last_actual_collect_time = time.monotonic()
duration = now2 - starttime
self._total_num_gc_objects += num_affected_objs
if (
num_affected_objs >= gc_threshold
and not self._showed_standard_mode_warning
):
loglevel: int = logging.WARNING
self._showed_standard_mode_warning = True
else:
loglevel = logging.INFO
log_is_visible = gc_log.isEnabledFor(loglevel)
obj_summary = ''
# Since we started with DEBUG_SAVEALL on, any objects we just
# collected should show up in gc.garbage. So we go through those
# objects, print stats or warnings as necessary, and then clear
# the list and run another gc pass without DEBUG_SAVEALL to
# *actually* kill them.
if num_affected_objs > 0:
# Build our summary of collected stuff ONLY if we'll actually
# be showing it.
if log_is_visible:
try:
obj_summary = _summarize_garbage(loglevel)
except Exception:
gc_log.exception('Error summarizing garbage.')
obj_summary = '(error in summarization)'
if len(gc.garbage) < num_affected_objs:
gc_log.debug(
(
'_collect_standard() collected %d objs but'
' only %d appear in gc.garbage; unexpected.'
),
num_affected_objs,
len(gc.garbage),
)
# *Actually* kill any stuff we found by temporarily turning
# *off save-all and running another collect.
if gc.garbage:
gc.set_debug(0)
gc.garbage.clear()
gc.collect()
gc.set_debug(gc.DEBUG_SAVEALL)
# We should have no garbage left at this point.
if gc.garbage:
gc_log.debug(
(
'Wound up with %d items in gc.garbage'
' after _collect_standard() cleanup; not expected.'
),
len(gc.garbage),
)
# Report some general stats on what we just did.
from_last = (
''
if self._last_collection_time is None
else f' from last {now - self._last_collection_time:.1f}s'
)
gc_log.log(
loglevel,
'Explicit gc pass handled %d objects%s in %.3fs (total: %d).%s',
num_affected_objs,
from_last,
duration,
self._total_num_gc_objects,
obj_summary,
)
def _collect_leak_debug(self, now: float) -> None:
starttime = now
num_affected_objs = gc.collect()
now2 = self.last_actual_collect_time = time.monotonic()
duration = now2 - starttime
self._total_num_gc_objects += num_affected_objs
# Just report some general stats on what we collected. The
# debugging output from Python itself will be the most useful
# thing here.
from_last = (
''
if self._last_collection_time is None
else f' from last {now - self._last_collection_time:.1f}s'
)
gc_log.info(
'Explicit gc pass handled %d objects%s in %.3fs (total: %d).',
num_affected_objs,
from_last,
duration,
self._total_num_gc_objects,
)
def _apply_mode(self, mode: Mode) -> None:
cls = type(mode)
if mode is cls.DISABLED:
# Do nothing.
pass
elif mode is cls.LEAK_DEBUG:
# For this mode we turn off collect, Enable printing all
# collected objects to stderr, and keep all collected
# objects around for introspection.
gc.disable()
gc.set_debug(gc.DEBUG_LEAK)
elif mode is cls.STANDARD:
# In this mode we turn off collect and keep all collected
# objects around for introspection. When we do an explicit
# collect we'll temporarily turn off save-all and *actually*
# delete collected stuff after examining/reporting it.
gc.disable()
gc.set_debug(gc.DEBUG_SAVEALL)
else:
assert_never(mode)
def _mode_from_config(self) -> Mode:
cfg = _babase.app.config
configval = cfg.get(self._MODE_CONFIG_KEY)
if configval is None:
mode = self.Mode.STANDARD
else:
try:
mode = self.Mode(configval)
except ValueError:
if _babase.do_once():
gc_log.warning(
"Invalid garbage-collection mode '%s'.", configval
)
mode = self.Mode.STANDARD
return mode
# Show some inline extra bits for specific types (such
# as type names for type objects).
def _inline_extra(tpname: str, type_paths: list[str]) -> str:
if tpname == 'type':
return ' (' + ', '.join(sorted(type_paths)) + ')'
return ''
def _summarize_garbage(loglevel: int) -> str:
"""Print stuff about gc.garbage to aid in breaking ref cycles."""
# pylint: disable=too-many-locals
import io
import traceback
from efro.debug import printrefs
debug_types: set[str]
debug_type_limit: int
plus = _babase.app.plus
if plus is None:
debug_types = set()
debug_type_limit = 1
else:
debug_types = set(plus.cloud.vals.gc_debug_types)
debug_type_limit = plus.cloud.vals.gc_debug_type_limit
debug_objs: dict[str, list[Any]] = {}
type_paths: list[str] = []
objtypecounts: dict[str, int] = {}
for obj in gc.garbage:
cls = type(obj)
if cls.__module__ == 'builtins':
tpname = cls.__qualname__
else:
tpname = f'{cls.__module__}.{cls.__qualname__}'
objtypecounts[tpname] = objtypecounts.get(tpname, 0) + 1
# Store specific objs for anything we're supposed to
# be debugging.
if tpname in debug_types and bool(True):
objs = debug_objs.setdefault(tpname, [])
if len(objs) < debug_type_limit:
objs.append(obj)
del objs
# Store type-names for types to show inline.
if tpname == 'type':
type_paths.append(f'{obj.__module__}.{obj.__qualname__}')
obj_summary = '\nObjects by type:' + ''.join(
f'\n {tpname}:' f' {tpcount}{_inline_extra(tpname, type_paths)}'
for tpname, tpcount in sorted(
objtypecounts.items(),
key=lambda i: (-i[1], i[0]),
)
)
for debug_obj_type, objs in sorted(debug_objs.items()):
for i, obj in enumerate(objs):
buffer = io.StringIO()
printrefs(obj, file=buffer)
buffer_indented = '\n'.join(
f' {line}' for line in buffer.getvalue().splitlines()
)
obj_summary += (
f'\n'
f'Refs for {debug_obj_type} {i+1} of {len(objs)}:\n'
f'{buffer_indented}'
)
if isinstance(obj, BaseException):
trace_str = ''.join(
traceback.format_exception(
type(obj), obj, obj.__traceback__
)
)
trace_str = '\n'.join(
f' {line}' for line in trace_str.splitlines()
)
obj_summary += (
f'\n'
f'Stack for {debug_obj_type} {i+1} of {len(objs)}:\n'
f'{trace_str}'
)
# Include overview if this a warning message.
if loglevel == logging.WARNING:
obj_summary += (
'\nToo many objects garbage-collected'
' - try to reduce this.\n'
'See babase.GarbageCollectionSubsystem documentation'
' to learn how.'
)
return obj_summary

View file

@ -34,7 +34,11 @@ DisplayTime = NewType('DisplayTime', float)
class Existable(Protocol): class Existable(Protocol):
"""A Protocol for objects supporting an ``exists()`` method.""" """A :class:`~typing.Protocol` for objects with an ``exists()`` method.
For more info about the concept of 'existables':
https://ballistica.net/wiki/Coding-Style-Guide
"""
def exists(self) -> bool: def exists(self) -> bool:
"""Whether this object exists.""" """Whether this object exists."""
@ -43,17 +47,17 @@ class Existable(Protocol):
def existing[ExistableT: Existable]( def existing[ExistableT: Existable](
obj: ExistableT | None, obj: ExistableT | None,
) -> ExistableT | None: ) -> ExistableT | None:
"""Convert invalid references to None for any babase.Existable object. """Convert invalid refs to None for an :class:`~babase.Existable`.
To best support type checking, it is important that invalid To best support type checking, it is important that invalid
references not be passed around and instead get converted to values references not be passed around and instead get converted to values
of None. That way the type checker can properly flag attempts to of None. That way the type checker can properly flag attempts to
pass possibly-dead objects (``FooType | None``) into functions pass possibly-dead objects (``FooType | None``) into functions
expecting only live ones (``FooType``), etc. This call can be used expecting only live ones (``FooType``), etc. This call can be used
on any 'existable' object (one with an ``exists()`` method) and will on any 'existable' object (one with an ``exists()`` method) to
convert it to a ``None`` value if it does not exist. convert it to ``None`` if it does not exist.
For more info, see notes on 'existables' here: For more info about the concept of 'existables':
https://ballistica.net/wiki/Coding-Style-Guide https://ballistica.net/wiki/Coding-Style-Guide
""" """
assert obj is None or hasattr(obj, 'exists'), f'No "exists" attr on {obj}.' assert obj is None or hasattr(obj, 'exists'), f'No "exists" attr on {obj}.'

View file

@ -174,12 +174,6 @@ def purchase_already_in_progress_error() -> None:
) )
def uuid_str() -> str:
import uuid
return str(uuid.uuid4())
def orientation_reset_cb_message() -> None: def orientation_reset_cb_message() -> None:
from babase._language import Lstr from babase._language import Lstr
@ -251,9 +245,9 @@ def unavailable_message() -> None:
def set_last_ad_network(sval: str) -> None: def set_last_ad_network(sval: str) -> None:
if _babase.app.classic is not None: if _babase.app.plus is not None:
_babase.app.classic.ads.last_ad_network = sval _babase.app.plus.ads.last_ad_network = sval
_babase.app.classic.ads.last_ad_network_set_time = time.time() _babase.app.plus.ads.last_ad_network_set_time = time.time()
def google_play_purchases_not_available_message() -> None: def google_play_purchases_not_available_message() -> None:
@ -305,8 +299,8 @@ def ui_remote_press() -> None:
def remove_in_game_ads_message() -> None: def remove_in_game_ads_message() -> None:
if _babase.app.classic is not None: if _babase.app.plus is not None:
_babase.app.classic.ads.do_remove_in_game_ads_message() _babase.app.plus.ads.do_remove_in_game_ads_message()
def do_quit() -> None: def do_quit() -> None:
@ -446,7 +440,7 @@ def copy_dev_console_history() -> None:
return return
# This requires us to be running with a log-handler set up. # This requires us to be running with a log-handler set up.
envconfig = baenv.get_config() envconfig = baenv.get_env_config()
if envconfig.log_handler is None: if envconfig.log_handler is None:
_babase.getsimplesound('error').play() _babase.getsimplesound('error').play()
_babase.screenmessage( _babase.screenmessage(

View file

@ -96,6 +96,12 @@ class LanguageSubsystem(AppSubsystem):
def _update_test_language(self, langid: str) -> None: def _update_test_language(self, langid: str) -> None:
if _babase.app.classic is None: if _babase.app.classic is None:
raise RuntimeError('This requires classic.') raise RuntimeError('This requires classic.')
# Only do this during normal running operation.
appstate = _babase.app.state
if appstate is not type(appstate).RUNNING:
return
_babase.app.classic.master_server_v1_get( _babase.app.classic.master_server_v1_get(
'bsLangGet', 'bsLangGet',
{'lang': langid, 'format': 'json'}, {'lang': langid, 'format': 'json'},

View file

@ -34,13 +34,21 @@ class LocaleSubsystem(AppSubsystem):
# the native layer. # the native layer.
env = _babase.env() env = _babase.env()
ba_locale = env.get('ba_locale') ba_locale = env.get('ba_locale')
locale_tag = env.get('locale') raw_locale_tag = env.get('locale')
if not isinstance(ba_locale, str) or not isinstance(locale_tag, str): if not isinstance(ba_locale, str) or not isinstance(
raw_locale_tag, str
):
applog.warning( applog.warning(
'Seem to be running in a dummy env; using en-US locale-tag.' 'Seem to be running in a dummy env; using en-US locale-tag.'
) )
ba_locale = '' ba_locale = ''
locale_tag = 'en-US' raw_locale_tag = 'en-US'
#: Raw locale string tag provided by the native layer. This will
#: be something in BCP 47 form (``en-US``) or POSIX locale form
#: (``en_US.UTF-8``). Generally you should use more well-defined
#: values such as :attr:`current_locale` instead of this.
self.raw_locale_tag: str = raw_locale_tag
#: The default locale based on the current runtime environment #: The default locale based on the current runtime environment
#: and app capabilities. This locale will be used unless the user #: and app capabilities. This locale will be used unless the user
@ -62,18 +70,17 @@ class LocaleSubsystem(AppSubsystem):
# Otherwise calc Locale from a tag ('en-US', etc.) # Otherwise calc Locale from a tag ('en-US', etc.)
if not have_valid_ba_locale: if not have_valid_ba_locale:
self.default_locale = LocaleResolved.from_tag(locale_tag).locale self.default_locale = LocaleResolved.from_tag(raw_locale_tag).locale
# If we can't properly display this default locale, set it to # If we can't properly display this default locale, set it to
# English instead. # English instead.
if ( if not self.can_display_locale(self.default_locale):
self.requires_full_unicode_display(self.default_locale.resolved)
and not _babase.supports_unicode_display()
):
self.default_locale = Locale.ENGLISH self.default_locale = Locale.ENGLISH
assert self.can_display_locale(self.default_locale)
@override @override
def do_apply_app_config(self) -> None: def apply_app_config(self) -> None:
""":meta private:""" """:meta private:"""
assert _babase.in_logic_thread() assert _babase.in_logic_thread()
assert isinstance(_babase.app.config, dict) assert isinstance(_babase.app.config, dict)
@ -114,62 +121,68 @@ class LocaleSubsystem(AppSubsystem):
@staticmethod @staticmethod
@cache @cache
def requires_full_unicode_display( def can_display_locale(locale: Locale) -> bool:
locale: LocaleResolved, """Are we able to display the passed locale?
) -> bool:
"""Does the locale require full unicode support to display?""" Some locales require integration with the OS to display the full
range of unicode text, which is not implemented on all
platforms.
"""
# pylint: disable=too-many-boolean-expressions # pylint: disable=too-many-boolean-expressions
cls = LocaleResolved cls = LocaleResolved
rlocale = locale.resolved
# DO need full unicode. # DO need full unicode.
if ( if (
locale is cls.CHINESE_TRADITIONAL rlocale is cls.CHINESE_TRADITIONAL
or locale is cls.CHINESE_SIMPLIFIED or rlocale is cls.CHINESE_SIMPLIFIED
or locale is cls.ARABIC or rlocale is cls.ARABIC
or locale is cls.HINDI or rlocale is cls.HINDI
or locale is cls.KOREAN or rlocale is cls.KOREAN
or locale is cls.PERSIAN or rlocale is cls.PERSIAN
or locale is cls.TAMIL or rlocale is cls.TAMIL
or locale is cls.THAI or rlocale is cls.THAI
or locale is cls.VIETNAMESE or rlocale is cls.VIETNAMESE
):
# Return True only if we can display full unicode.
return _babase.supports_unicode_display()
# Do NOT need full unicode; can always display.
if (
rlocale is cls.ENGLISH
or rlocale is cls.PORTUGUESE_PORTUGAL
or rlocale is cls.PORTUGUESE_BRAZIL
or rlocale is cls.BELARUSSIAN
or rlocale is cls.CROATIAN
or rlocale is cls.CZECH
or rlocale is cls.DANISH
or rlocale is cls.DUTCH
or rlocale is cls.PIRATE_SPEAK
or rlocale is cls.ESPERANTO
or rlocale is cls.FILIPINO
or rlocale is cls.FRENCH
or rlocale is cls.GERMAN
or rlocale is cls.GIBBERISH
or rlocale is cls.GREEK
or rlocale is cls.HUNGARIAN
or rlocale is cls.INDONESIAN
or rlocale is cls.ITALIAN
or rlocale is cls.MALAY
or rlocale is cls.POLISH
or rlocale is cls.ROMANIAN
or rlocale is cls.RUSSIAN
or rlocale is cls.SERBIAN
or rlocale is cls.SPANISH_LATIN_AMERICA
or rlocale is cls.SPANISH_SPAIN
or rlocale is cls.SLOVAK
or rlocale is cls.SWEDISH
or rlocale is cls.TURKISH
or rlocale is cls.UKRAINIAN
or rlocale is cls.VENETIAN
or rlocale is cls.KAZAKH
): ):
return True return True
# Do NOT need full unicode.
if (
locale is cls.ENGLISH
or locale is cls.PORTUGUESE_PORTUGAL
or locale is cls.PORTUGUESE_BRAZIL
or locale is cls.BELARUSSIAN
or locale is cls.CROATIAN
or locale is cls.CZECH
or locale is cls.DANISH
or locale is cls.DUTCH
or locale is cls.PIRATE_SPEAK
or locale is cls.ESPERANTO
or locale is cls.FILIPINO
or locale is cls.FRENCH
or locale is cls.GERMAN
or locale is cls.GIBBERISH
or locale is cls.GREEK
or locale is cls.HUNGARIAN
or locale is cls.INDONESIAN
or locale is cls.ITALIAN
or locale is cls.MALAY
or locale is cls.POLISH
or locale is cls.ROMANIAN
or locale is cls.RUSSIAN
or locale is cls.SERBIAN
or locale is cls.SPANISH_LATIN_AMERICA
or locale is cls.SPANISH_SPAIN
or locale is cls.SLOVAK
or locale is cls.SWEDISH
or locale is cls.TURKISH
or locale is cls.UKRAINIAN
or locale is cls.VENETIAN
):
return False
# Make sure we're covering all cases. # Make sure we're covering all cases.
assert_never(locale) assert_never(rlocale)

View file

@ -6,7 +6,48 @@ from __future__ import annotations
import logging import logging
# Our standard set of loggers. from bacommon.logging import ClientLoggerName
balog = logging.getLogger('ba')
applog = logging.getLogger('ba.app') # Keep a dict of logger descriptions so lookup is speedy, but lazy-init
lifecyclelog = logging.getLogger('ba.lifecycle') # it since most users won't need it.
_g_logger_descs: dict[str, str] | None = None
# Common loggers we may want convenient access to.
balog = logging.getLogger(ClientLoggerName.BA.value)
applog = logging.getLogger(ClientLoggerName.APP.value)
assetslog = logging.getLogger(ClientLoggerName.ASSETS.value)
audiolog = logging.getLogger(ClientLoggerName.AUDIO.value)
cachelog = logging.getLogger(ClientLoggerName.CACHE.value)
displaytimelog = logging.getLogger(ClientLoggerName.DISPLAYTIME.value)
gc_log = logging.getLogger(ClientLoggerName.GARBAGE_COLLECTION.value)
gfxlog = logging.getLogger(ClientLoggerName.GRAPHICS.value)
perflog = logging.getLogger(ClientLoggerName.PERFORMANCE.value)
inputlog = logging.getLogger(ClientLoggerName.INPUT.value)
lifecyclelog = logging.getLogger(ClientLoggerName.LIFECYCLE.value)
netlog = logging.getLogger(ClientLoggerName.NETWORKING.value)
connectivitylog = logging.getLogger(ClientLoggerName.CONNECTIVITY.value)
v2transportlog = logging.getLogger(ClientLoggerName.V2TRANSPORT.value)
cloudsublog = logging.getLogger(ClientLoggerName.CLOUD_SUBSCRIPTION.value)
accountlog = logging.getLogger(ClientLoggerName.ACCOUNT.value)
accountclientv2log = logging.getLogger(ClientLoggerName.ACCOUNT_CLIENT_V2.value)
loginadapterlog = logging.getLogger(ClientLoggerName.LOGIN_ADAPTER.value)
def description_for_logger(logger: str) -> str | None:
"""Return a short description for a given logger.
Used to populate the logger control dev console tab.
"""
global _g_logger_descs # pylint: disable=global-statement
if _g_logger_descs is None:
# Describe a few specific loggers here and also include our
# client logger descriptions.
_g_logger_descs = {
'root': 'top level Python logger - use to adjust everything',
'asyncio': 'Python\'s async/await functionality',
}
for clientlogger in ClientLoggerName:
_g_logger_descs[clientlogger.value] = clientlogger.description
return _g_logger_descs.get(logger)

View file

@ -12,13 +12,12 @@ from typing import TYPE_CHECKING, final, override
from bacommon.login import LoginType from bacommon.login import LoginType
from babase._logging import loginadapterlog
import _babase import _babase
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Callable from typing import Callable
logger = logging.getLogger('ba.loginadapter')
@dataclass @dataclass
class LoginInfo: class LoginInfo:
@ -97,12 +96,12 @@ class LoginAdapter:
return return
if state is None: if state is None:
logger.debug( loginadapterlog.debug(
'%s implicit state changed; now signed out.', '%s implicit state changed; now signed out.',
self.login_type.name, self.login_type.name,
) )
else: else:
logger.debug( loginadapterlog.debug(
'%s implicit state changed; now signed in as %s.', '%s implicit state changed; now signed in as %s.',
self.login_type.name, self.login_type.name,
state.display_name, state.display_name,
@ -129,7 +128,7 @@ class LoginAdapter:
:meta private: :meta private:
""" """
assert _babase.in_logic_thread() assert _babase.in_logic_thread()
logger.debug( loginadapterlog.debug(
'%s adapter got active logins %s.', '%s adapter got active logins %s.',
self.login_type.name, self.login_type.name,
{k: v[:4] + '...' + v[-4:] for k, v in logins.items()}, {k: v[:4] + '...' + v[-4:] for k, v in logins.items()},
@ -197,7 +196,7 @@ class LoginAdapter:
self._last_sign_in_desc = description self._last_sign_in_desc = description
self._last_sign_in_time = now self._last_sign_in_time = now
logger.debug( loginadapterlog.debug(
'%s adapter sign_in() called; fetching sign-in-token...', '%s adapter sign_in() called; fetching sign-in-token...',
self.login_type.name, self.login_type.name,
) )
@ -207,7 +206,7 @@ class LoginAdapter:
# Failed to get a sign-in-token. # Failed to get a sign-in-token.
if result is None: if result is None:
logger.debug( loginadapterlog.debug(
'%s adapter sign-in-token fetch failed;' '%s adapter sign-in-token fetch failed;'
' aborting sign-in.', ' aborting sign-in.',
self.login_type.name, self.login_type.name,
@ -224,7 +223,7 @@ class LoginAdapter:
# Got a sign-in token! Now pass it to the cloud which will use # Got a sign-in token! Now pass it to the cloud which will use
# it to verify our identity and give us app credentials on # it to verify our identity and give us app credentials on
# success. # success.
logger.debug( loginadapterlog.debug(
'%s adapter sign-in-token fetch succeeded;' '%s adapter sign-in-token fetch succeeded;'
' passing to cloud for verification...', ' passing to cloud for verification...',
self.login_type.name, self.login_type.name,
@ -235,7 +234,7 @@ class LoginAdapter:
) -> None: ) -> None:
# This likely means we couldn't communicate with the server. # This likely means we couldn't communicate with the server.
if isinstance(response, Exception): if isinstance(response, Exception):
logger.debug( loginadapterlog.debug(
'%s adapter got error sign-in response: %s', '%s adapter got error sign-in response: %s',
self.login_type.name, self.login_type.name,
response, response,
@ -248,7 +247,7 @@ class LoginAdapter:
RuntimeError('Sign-in-token was rejected.') RuntimeError('Sign-in-token was rejected.')
) )
else: else:
logger.debug( loginadapterlog.debug(
'%s adapter got successful sign-in response', '%s adapter got successful sign-in response',
self.login_type.name, self.login_type.name,
) )
@ -298,7 +297,7 @@ class LoginAdapter:
# any existing state so it can properly respond to this. # any existing state so it can properly respond to this.
if self._implicit_login_state_dirty and self._on_app_loading_called: if self._implicit_login_state_dirty and self._on_app_loading_called:
logger.debug( loginadapterlog.debug(
'%s adapter sending implicit-state-changed to app.', '%s adapter sending implicit-state-changed to app.',
self.login_type.name, self.login_type.name,
) )
@ -322,7 +321,7 @@ class LoginAdapter:
self._implicit_login_state.login_id == self._active_login_id self._implicit_login_state.login_id == self._active_login_id
) )
if was_active != is_active: if was_active != is_active:
logger.debug( loginadapterlog.debug(
'%s adapter back-end-active is now %s.', '%s adapter back-end-active is now %s.',
self.login_type.name, self.login_type.name,
is_active, is_active,

View file

@ -23,6 +23,8 @@ if TYPE_CHECKING:
# This is purely a convenience; it is possible to use full class paths # 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. # instead of these or to make the meta system aware of arbitrary classes.
EXPORT_CLASS_NAME_SHORTCUTS: dict[str, str] = { EXPORT_CLASS_NAME_SHORTCUTS: dict[str, str] = {
# DEPRECATED as of 6/2025. Currently am warning if finding these
# but should take this out eventually.
'plugin': 'babase.Plugin', 'plugin': 'babase.Plugin',
# DEPRECATED as of 12/2023. Currently am warning if finding these # DEPRECATED as of 12/2023. Currently am warning if finding these
# but should take this out eventually. # but should take this out eventually.
@ -42,12 +44,6 @@ class ScanResults:
"""Return exports matching a given name.""" """Return exports matching a given name."""
return self.exports.get(name, []) return self.exports.get(name, [])
# def exports_of_class(self, cls: type) -> list[str]:
# """Return exports of a given class."""
# print('RETURNING', cls)
# return self.exports.get(f'{cls.__module__}.{cls.__qualname__}', [])
class MetadataSubsystem: class MetadataSubsystem:
"""Subsystem for working with script metadata in the app. """Subsystem for working with script metadata in the app.
@ -92,7 +88,7 @@ class MetadataSubsystem:
] ]
) )
Thread(target=self._run_scan_in_bg, daemon=True).start() Thread(target=self._run_scan_in_bg).start()
def start_extra_scan(self) -> None: def start_extra_scan(self) -> None:
"""Proceed to the extra_scan_dirs portion of the scan. """Proceed to the extra_scan_dirs portion of the scan.
@ -130,8 +126,7 @@ class MetadataSubsystem:
cls, cls,
completion_cb, completion_cb,
completion_cb_in_bg_thread, completion_cb_in_bg_thread,
), )
daemon=True,
).start() ).start()
def _load_exported_classes[T]( def _load_exported_classes[T](
@ -436,8 +431,20 @@ class DirectoryScan:
if export_class_name is not None: if export_class_name is not None:
classname = modulename + '.' + export_class_name classname = modulename + '.' + export_class_name
# Migrating away from the 'keyboard' name shortcut # Migrating away from the 'plugin' name shortcut;
# since it's specific to bauiv1; warn if we find it. # warn if we find it.
if exporttypestr == 'plugin':
logging.warning(
"metascan: %s:%d: '# ba_meta export"
" plugin' tag should be replaced by '# ba_meta"
" export babase.Plugin'.",
subpath,
lindex + 1,
)
self.results.announce_errors_occurred = True
# Migrating away from the 'keyboard' name shortcut;
# warn if we find it.
if exporttypestr == 'keyboard': if exporttypestr == 'keyboard':
logging.warning( logging.warning(
"metascan: %s:%d: '# ba_meta export" "metascan: %s:%d: '# ba_meta export"

View file

@ -184,3 +184,4 @@ class SpecialChar(Enum):
FLAG_CHILE = 94 FLAG_CHILE = 94
MIKIROG = 95 MIKIROG = 95
V2_LOGO = 96 V2_LOGO = 96
CLOSE = 97

View file

@ -3,7 +3,6 @@
"""Networking related functionality.""" """Networking related functionality."""
from __future__ import annotations from __future__ import annotations
import ssl
import socket import socket
import threading import threading
import ipaddress import ipaddress
@ -12,17 +11,24 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
pass pass
# Timeout for standard functions talking to the master-server/etc.
DEFAULT_REQUEST_TIMEOUT_SECONDS = 60
class NetworkSubsystem: class NetworkSubsystem:
"""Network related app subsystem.""" """Network related app subsystem."""
def __init__(self) -> None: def __init__(self) -> None:
# Our shared SSL context. Creating these can be expensive so we import babase._env
# create it here once and recycle for our various connections.
self.sslcontext = ssl.create_default_context() assert babase._env._g_net_warm_start_thread is not None
babase._env._g_net_warm_start_thread.join()
babase._env._g_net_warm_start_thread = None
assert babase._env._g_net_warm_start_ssl_context is not None
self.sslcontext = babase._env._g_net_warm_start_ssl_context
babase._env._g_net_warm_start_ssl_context = None
assert babase._env._g_net_warm_start_pool_manager is not None
self.urllib3pool = babase._env._g_net_warm_start_pool_manager
babase._env._g_net_warm_start_pool_manager = None
# Anyone accessing/modifying zone_pings should hold this lock, # Anyone accessing/modifying zone_pings should hold this lock,
# as it is updated by a background thread. # as it is updated by a background thread.
@ -34,12 +40,14 @@ class NetworkSubsystem:
self.zone_pings: dict[str, float] = {} self.zone_pings: dict[str, float] = {}
# For debugging/progress. # For debugging/progress.
self.v1_test_log: str = '' self.connectivity_state = ''
self.v1_ctest_results: dict[int, str] = {} self.transport_state = ''
self.connectivity_state = 'uninited'
self.transport_state = 'uninited'
self.server_time_offset_hours: float | None = None self.server_time_offset_hours: float | None = None
def pre_interpreter_shutdown(self) -> None:
"""Called just before interpreter shuts down."""
self.urllib3pool.clear()
def get_ip_address_type(addr: str) -> socket.AddressFamily: def get_ip_address_type(addr: str) -> socket.AddressFamily:
"""Return an address-type given an address. """Return an address-type given an address.

View file

@ -54,8 +54,7 @@ class WorkspaceSubsystem:
workspaceid=workspaceid, workspaceid=workspaceid,
workspacename=workspacename, workspacename=workspacename,
on_completed=on_completed, on_completed=on_completed,
), )
daemon=True,
).start() ).start()
def _errmsg(self, msg: babase.Lstr) -> None: def _errmsg(self, msg: babase.Lstr) -> None:
@ -73,6 +72,7 @@ class WorkspaceSubsystem:
workspacename: str, workspacename: str,
on_completed: Callable[[], None], on_completed: Callable[[], None],
) -> None: ) -> None:
# pylint: disable=too-many-locals
from babase._language import Lstr from babase._language import Lstr
class _SkipSyncError(RuntimeError): class _SkipSyncError(RuntimeError):
@ -83,22 +83,32 @@ class WorkspaceSubsystem:
set_path = True set_path = True
wspath = Path( wspath = Path(
_babase.get_volatile_data_directory(), 'workspaces', workspaceid _babase.app.env.cache_directory, 'workspaces', workspaceid
) )
try: try:
# If it seems we're offline, don't even attempt a sync, # If it seems we're offline, don't even attempt a sync, but
# but allow using the previous synced state. # allow using the previous synced state. (is this a good
# (is this a good idea?) # idea?)
if not plus.cloud.is_connected(): if not plus.cloud.is_connected():
raise _SkipSyncError() raise _SkipSyncError()
manifest = DirectoryManifest.create_from_disk(wspath) manifest = DirectoryManifest.create_from_disk(wspath)
# FIXME: Should implement a way to pass account credentials in # FIXME: Should implement a way to pass account credentials
# from the logic thread. # in from the logic thread.
state = bacommon.cloud.WorkspaceFetchState(manifest=manifest) state = bacommon.cloud.WorkspaceFetchState(manifest=manifest)
while True: while True:
# Abort if the app is shutting down.
appstate = _babase.app.state
appstate_t = type(appstate)
if (
appstate is appstate_t.SHUTTING_DOWN
or appstate is appstate_t.SHUTDOWN_COMPLETE
):
break
with account: with account:
response = plus.cloud.send_message( response = plus.cloud.send_message(
bacommon.cloud.WorkspaceFetchMessage( bacommon.cloud.WorkspaceFetchMessage(
@ -144,8 +154,8 @@ class WorkspaceSubsystem:
) )
except CleanError as exc: except CleanError as exc:
# Avoid reusing existing if we fail in the middle; could # Avoid reusing existing if we fail in the middle; could be
# be in wonky state. # in wonky state.
set_path = False set_path = False
_babase.pushcall( _babase.pushcall(
partial(self._errmsg, Lstr(value=str(exc))), partial(self._errmsg, Lstr(value=str(exc))),
@ -167,8 +177,8 @@ class WorkspaceSubsystem:
) )
if set_path and wspath.is_dir(): if set_path and wspath.is_dir():
# Add to Python paths and also to list of stuff to be scanned # Add to Python paths and also to list of stuff to be
# for meta tags. # scanned for meta tags.
sys.path.insert(0, str(wspath)) sys.path.insert(0, str(wspath))
_babase.app.meta.extra_scan_dirs.append(str(wspath)) _babase.app.meta.extra_scan_dirs.append(str(wspath))
@ -191,9 +201,10 @@ class WorkspaceSubsystem:
"""Handle inline file data to be saved to the client.""" """Handle inline file data to be saved to the client."""
for fname, fdata in downloads_inline.items(): for fname, fdata in downloads_inline.items():
fname = os.path.join(workspace_dir, fname) fname = os.path.join(workspace_dir, fname)
# If there's a directory where we want our file to go, clear it # If there's a directory where we want our file to go, clear
# out first. File deletes should have run before this so # it out first. File deletes should have run before this so
# everything under it should be empty and thus killable via rmdir. # everything under it should be empty and thus killable via
# rmdir.
if os.path.isdir(fname): if os.path.isdir(fname):
for basename, dirnames, _fn in os.walk(fname, topdown=False): for basename, dirnames, _fn in os.walk(fname, topdown=False):
for dirname in dirnames: for dirname in dirnames:
@ -208,7 +219,8 @@ class WorkspaceSubsystem:
def _handle_dir_prune_empty(self, prunedir: str) -> None: def _handle_dir_prune_empty(self, prunedir: str) -> None:
"""Handle pruning empty directories.""" """Handle pruning empty directories."""
# Walk the tree bottom-up so we can properly kill recursive empty dirs. # Walk the tree bottom-up so we can properly kill recursive
# empty dirs.
for basename, dirnames, filenames in os.walk(prunedir, topdown=False): for basename, dirnames, filenames in os.walk(prunedir, topdown=False):
# It seems that child dirs we kill during the walk are still # It seems that child dirs we kill during the walk are still
# listed when the parent dir is visited, so lets make sure # listed when the parent dir is visited, so lets make sure

View file

@ -11,14 +11,14 @@ designed in a more modular way.
# ba_meta require api 9 # ba_meta require api 9
# Note: Code relying on classic should import things from here *only* # Note: Stuff in this module mostly exists for type-checking and docs
# for type-checking and use the versions in ba*.app.classic at runtime; # generation and should generally not be imported or used at runtime.
# that way type-checking will cleanly cover the classic-not-present case # Generally all interaction with this feature-set should go through
# (ba*.app.classic being None). # `ba*.app.classic`.
import logging import logging
# from efro.util import set_canonical_module_names from _baclassic import reload_hooks
from baclassic._appmode import ClassicAppMode from baclassic._appmode import ClassicAppMode
from baclassic._appsubsystem import ClassicAppSubsystem from baclassic._appsubsystem import ClassicAppSubsystem
from baclassic._achievement import Achievement, AchievementSubsystem from baclassic._achievement import Achievement, AchievementSubsystem
@ -40,6 +40,7 @@ __all__ = [
'AchievementSubsystem', 'AchievementSubsystem',
'show_display_item', 'show_display_item',
'MusicPlayer', 'MusicPlayer',
'reload_hooks',
] ]
# We want stuff here to show up as packagename.Foo instead of # We want stuff here to show up as packagename.Foo instead of

View file

@ -168,9 +168,10 @@ class AccountV1Subsystem:
"""(internal)""" """(internal)"""
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
plus = babase.app.plus plus = babase.app.plus
if plus is None: classic = babase.app.classic
if plus is None or classic is None:
return [] return []
if plus.get_v1_account_state() != 'signed_in': if plus.accounts.primary is None:
return [] return []
icons = [] icons = []
store_items: dict[str, Any] = ( store_items: dict[str, Any] = (
@ -179,9 +180,10 @@ class AccountV1Subsystem:
else {} else {}
) )
for item_name, item in list(store_items.items()): for item_name, item in list(store_items.items()):
if item_name.startswith( if (
'icons.' item_name.startswith('icons.')
) and plus.get_v1_account_product_purchased(item_name): and item_name in classic.purchases
):
icons.append(item['icon']) icons.append(item['icon'])
return icons return icons
@ -228,16 +230,17 @@ class AccountV1Subsystem:
def have_pro(self) -> bool: def have_pro(self) -> bool:
"""Return whether pro is currently unlocked.""" """Return whether pro is currently unlocked."""
plus = babase.app.plus classic = babase.app.classic
if plus is None: if classic is None:
return False return False
purchases = classic.purchases
# Check various server-side purchases that mean we have pro. # Check various server-side purchases that mean we have pro.
return bool( return (
plus.get_v1_account_product_purchased('gold_pass') 'gold_pass' in purchases
or plus.get_v1_account_product_purchased('upgrades.pro') or 'upgrades.pro' in purchases
or plus.get_v1_account_product_purchased('static.pro') or 'static.pro' in purchases
or plus.get_v1_account_product_purchased('static.pro_sale') or 'static.pro_sale' in purchases
) )
def have_pro_options(self) -> bool: def have_pro_options(self) -> bool:

View file

@ -1,14 +1,17 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
"""Contains ClassicAppMode.""" """Contains ClassicAppMode."""
# pylint: disable=too-many-lines
from __future__ import annotations from __future__ import annotations
import os import os
import logging import logging
import hashlib
from functools import partial from functools import partial
from typing import TYPE_CHECKING, override from typing import TYPE_CHECKING, override
# from bacommon.app import AppExperience from efro.error import CommunicationError
import bacommon.bs import bacommon.bs
import babase import babase
import bauiv1 import bauiv1
@ -18,7 +21,7 @@ from bauiv1lib.account.signin import show_sign_in_prompt
import _baclassic import _baclassic
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Callable, Any, Literal from typing import Callable, Any, Literal, Iterable
from efro.call import CallbackRegistration from efro.call import CallbackRegistration
import bacommon.cloud import bacommon.cloud
@ -29,7 +32,8 @@ if TYPE_CHECKING:
class ClassicAppMode(babase.AppMode): class ClassicAppMode(babase.AppMode):
"""AppMode for the classic BombSquad experience.""" """AppMode for the classic BombSquad experience."""
_LEAGUE_VIS_VALS_CONFIG_KEY = 'ClassicLeagueVisVals' _ACCOUNT_STATE_CONFIG_KEY = 'ClassicAccountState'
_ASKED_FOR_REVIEW_CONFIG_KEY = 'AskedForReview'
def __init__(self) -> None: def __init__(self) -> None:
self._on_primary_account_changed_callback: ( self._on_primary_account_changed_callback: (
@ -44,11 +48,18 @@ class ClassicAppMode(babase.AppMode):
self._have_account_values = False self._have_account_values = False
self._have_connectivity = False self._have_connectivity = False
self._current_account_id: str | None = None self._current_account_id: str | None = None
self._should_restore_account_display_state = False
self._purchase_ui_pause: bauiv1.RootUIUpdatePause | None = None self._purchase_ui_pause: bauiv1.RootUIUpdatePause | None = None
self._last_tokens_value = 0 self._last_tokens_value = 0
self._purchases_update_timer: babase.AppTimer | None = None
self._purchase_request_in_flight = False
self._target_purchases_state: str | None = None
# state-hash and purchases we last pushed to the classic subsystem
self._current_purchases_state: str | None = None
self._current_purchases: frozenset[str] | None = None
@override @override
@classmethod @classmethod
def can_handle_intent(cls, intent: babase.AppIntent) -> bool: def can_handle_intent(cls, intent: babase.AppIntent) -> bool:
@ -152,7 +163,7 @@ class ClassicAppMode(babase.AppMode):
classic = babase.app.classic classic = babase.app.classic
# Store latest league vis vals for any active account. # Store latest league vis vals for any active account.
self._save_account_display_state() self._save_account_state()
# Stop being informed of account changes. # Stop being informed of account changes.
self._on_primary_account_changed_callback = None self._on_primary_account_changed_callback = None
@ -173,13 +184,14 @@ class ClassicAppMode(babase.AppMode):
@override @override
def on_app_active_changed(self) -> None: def on_app_active_changed(self) -> None:
if not babase.app.active: if not babase.app.active:
# If we've gone inactive, bring up the main menu, which has the # If we're going inactive, ask for the main ui, which should
# side effect of pausing the action (when possible). # have the side effect of pausing the action if we're in a
babase.invoke_main_menu() # game.
babase.request_main_ui()
# Also store any league vis state for the active account. # Also store any league vis state for the active account.
# this may be our last chance to do this on mobile. # this may be our last chance to do this on mobile.
self._save_account_display_state() self._save_account_state()
@override @override
def on_purchase_process_begin( def on_purchase_process_begin(
@ -304,7 +316,7 @@ class ClassicAppMode(babase.AppMode):
This happens at various times such as session switches. This happens at various times such as session switches.
""" """
self._save_account_display_state() self._save_account_state()
def on_engine_did_reset(self) -> None: def on_engine_did_reset(self) -> None:
"""Called just after classic resets the engine. """Called just after classic resets the engine.
@ -315,7 +327,79 @@ class ClassicAppMode(babase.AppMode):
# Restore any old league vis state we had; this allows the user # Restore any old league vis state we had; this allows the user
# to see animations for league improvements or other changes # to see animations for league improvements or other changes
# that have occurred since the last time we were visible. # that have occurred since the last time we were visible.
self._restore_account_display_state() self._restore_account_state()
def _update_purchases(self) -> None:
self._possibly_request_purchases()
def _possibly_request_purchases(self) -> None:
if self._purchase_request_in_flight:
return
self._purchase_request_in_flight = True
babase.accountlog.debug('Requesting latest purchases state...')
plus = babase.app.plus
assert plus is not None
if plus.accounts.primary is None:
raise RuntimeError(
'No account present when requesting classic purchases.'
)
with plus.accounts.primary:
plus.cloud.send_message_cb(
bacommon.bs.GetClassicPurchasesMessage(),
on_response=babase.WeakCall(
self._on_get_classic_purchases_response
),
)
def _on_get_classic_purchases_response(
self, response: bacommon.bs.GetClassicPurchasesResponse | Exception
) -> None:
assert self._purchase_request_in_flight
self._purchase_request_in_flight = False
if isinstance(response, Exception):
if isinstance(response, CommunicationError):
# No biggie; we expect these when offline/etc.
pass
else:
babase.netlog.exception('Error requesting classic purchases.')
return
# If we're no longer looking for a state, we can abort early.
if self._target_purchases_state is None:
babase.accountlog.debug(
'No longer looking for new purchases state; aborting fetch.'
)
self._purchases_update_timer = None
return
state = self._state_from_purchases(response.purchases)
# If this is NOT the state we're after, ignore and keep going.
if state != self._target_purchases_state:
return
# Ok, this is what we were after. Store a frozen version of it
# and its hash and push it to the classic subsystem.
self._current_purchases = frozenset(response.purchases)
self._current_purchases_state = state
assert babase.app.classic is not None
babase.app.classic.purchases = self._current_purchases
babase.accountlog.debug(
'Updated purchases state to %s: (%s items)',
state,
len(self._current_purchases),
)
self._purchases_update_timer = None
@staticmethod
def _state_from_purchases(purchases: Iterable[str]) -> str:
return hashlib.md5(','.join(sorted(purchases)).encode()).hexdigest()
def _update_for_primary_account( def _update_for_primary_account(
self, account: babase.AccountV2Handle | None self, account: babase.AccountV2Handle | None
@ -331,17 +415,10 @@ class ClassicAppMode(babase.AppMode):
if account is not None: if account is not None:
self._current_account_id = account.accountid self._current_account_id = account.accountid
babase.set_ui_account_state(True, account.tag) self._restore_account_state()
self._should_restore_account_display_state = True
else: else:
# If we had an account, save any existing league vis state self._save_account_state()
# so we'll properly animate to new values the next time we
# sign in.
self._save_account_display_state()
self._current_account_id = None self._current_account_id = None
babase.set_ui_account_state(False)
self._should_restore_account_display_state = False
# For testing subscription functionality. # For testing subscription functionality.
if os.environ.get('BA_SUBSCRIPTION_TEST') == '1': if os.environ.get('BA_SUBSCRIPTION_TEST') == '1':
@ -358,8 +435,11 @@ class ClassicAppMode(babase.AppMode):
if account is None: if account is None:
classic.gold_pass = False classic.gold_pass = False
classic.tokens = 0 classic.tokens = 0
classic.tickets = 0
classic.purchases = frozenset()
classic.chest_dock_full = False classic.chest_dock_full = False
classic.remove_ads = False classic.remove_ads = False
self._target_purchases_state = None
self._account_data_sub = None self._account_data_sub = None
_baclassic.set_root_ui_account_values( _baclassic.set_root_ui_account_values(
tickets=-1, tickets=-1,
@ -399,6 +479,8 @@ class ClassicAppMode(babase.AppMode):
self._update_ui_live_state() self._update_ui_live_state()
else: else:
# Establish a subscription to inform us whenever basic stuff
# (token count, chests, etc) changes.
with account: with account:
self._account_data_sub = ( self._account_data_sub = (
plus.cloud.subscribe_classic_account_data( plus.cloud.subscribe_classic_account_data(
@ -429,9 +511,8 @@ class ClassicAppMode(babase.AppMode):
self, val: bacommon.bs.ClassicAccountLiveData self, val: bacommon.bs.ClassicAccountLiveData
) -> None: ) -> None:
achp = round(val.achievements / max(val.achievements_total, 1) * 100.0) achp = round(val.achievements / max(val.achievements_total, 1) * 100.0)
# ibc = str(val.inbox_count)
# if val.inbox_count_is_max: babase.accountlog.debug('Got new classic account data.')
# ibc += '+'
chest0 = val.chests.get('0') chest0 = val.chests.get('0')
chest1 = val.chests.get('1') chest1 = val.chests.get('1')
@ -445,6 +526,55 @@ class ClassicAppMode(babase.AppMode):
classic.remove_ads = val.remove_ads classic.remove_ads = val.remove_ads
classic.gold_pass = val.gold_pass classic.gold_pass = val.gold_pass
classic.tokens = val.tokens classic.tokens = val.tokens
classic.tickets = val.tickets
self._target_purchases_state = val.purchases_state
# If they want us to ask for a review (and we haven't yet), do
# so.
if val.Flag.ASK_FOR_REVIEW in val.flags:
cfg = babase.app.config
if (
not cfg.get(self._ASKED_FOR_REVIEW_CONFIG_KEY, False)
and babase.native_review_request_supported()
):
cfg[self._ASKED_FOR_REVIEW_CONFIG_KEY] = True
cfg.commit()
babase.native_review_request()
# If someone replaced our purchases in the classic subsystem,
# fix it.
if (
self._current_purchases is not None
and self._current_purchases is not classic.purchases
):
classic.purchases = self._current_purchases
# If we need to fetch purchases, set up a timer to do so until
# successful and possibly kick off an immediate attempt.
if (
self._target_purchases_state is not None
and self._current_purchases_state != self._target_purchases_state
):
babase.accountlog.debug(
'Account purchases state is %s; we have %s. Will fetch new.',
self._target_purchases_state,
self._current_purchases_state,
)
if self._purchases_update_timer is not None:
# Ok there's already a timer going; just let it keep
# doing its thing.
pass
else:
self._purchases_update_timer = babase.AppTimer(
3.456, self._update_purchases, repeat=True
)
self._possibly_request_purchases()
else:
# Not dirty; don't need a timer.
self._purchases_update_timer = None
classic.chest_dock_full = ( classic.chest_dock_full = (
chest0 is not None chest0 is not None
and chest1 is not None and chest1 is not None
@ -540,21 +670,13 @@ class ClassicAppMode(babase.AppMode):
else chest3.ad_allow_time.timestamp() else chest3.ad_allow_time.timestamp()
), ),
) )
if self._should_restore_account_display_state:
# If we have a previous display-state for this account,
# restore it. This will cause us to animate or otherwise
# display league changes that have occurred since we were
# last visible. Note we need to do this *after* setting real
# vals so there is a current state to animate to.
self._restore_account_display_state()
self._should_restore_account_display_state = False
# Note that we have values and updated faded state accordingly. # Note that we have values and updated faded state accordingly.
self._have_account_values = True self._have_account_values = True
self._update_ui_live_state() self._update_ui_live_state()
def _root_ui_menu_press(self) -> None: def _root_ui_menu_press(self) -> None:
from babase import push_back_press from babase import menu_press
ui = babase.app.ui_v1 ui = babase.app.ui_v1
@ -562,15 +684,16 @@ class ClassicAppMode(babase.AppMode):
old_window = ui.get_main_window() old_window = ui.get_main_window()
if old_window is not None: if old_window is not None:
bauiv1.getsound('swish').play()
classic = babase.app.classic classic = babase.app.classic
assert classic is not None assert classic is not None
classic.resume() classic.resume()
ui.clear_main_window() ui.clear_main_window()
return else:
# Otherwise act like a standard menu button.
# Otherwise menu_press()
push_back_press()
def _root_ui_account_press(self) -> None: def _root_ui_account_press(self) -> None:
from bauiv1lib.account.settings import AccountSettingsWindow from bauiv1lib.account.settings import AccountSettingsWindow
@ -866,37 +989,59 @@ class ClassicAppMode(babase.AppMode):
) )
) )
def _save_account_display_state(self) -> None: def _save_account_state(self) -> None:
if self._current_account_id is None:
return
# If we currently have an account, save the state of what we're vals = _baclassic.get_account_state()
# currently displaying for it in the root ui/etc. We'll then if vals is None:
# restore that state as a starting point the next time we are return
# active. This allows things like league rank changes to be
# properly animated even if they occurred while we were offline
# or while the UI was hidden.
if self._current_account_id is not None: # Stuff some vals of our own in the dict and save to config.
vals = _baclassic.get_account_display_state()
if vals is not None:
# Stuff our account id in there and save it to our
# config.
assert 'a' not in vals assert 'a' not in vals
vals['a'] = self._current_account_id vals['a'] = self._current_account_id
assert babase.app.classic is not None
assert 'p' not in vals
vals['p'] = list(babase.app.classic.purchases)
cfg = babase.app.config cfg = babase.app.config
cfg[self._LEAGUE_VIS_VALS_CONFIG_KEY] = vals cfg[self._ACCOUNT_STATE_CONFIG_KEY] = vals
cfg.commit() cfg.commit()
def _restore_account_display_state(self) -> None: def _restore_account_state(self) -> None:
# If we've got a stored state for the current account, restore
# it.
assert babase.app.classic is not None
if self._current_account_id is None:
return
# If we currently have an account and it matches the
# display-state we have stored in the config, restore the state.
if self._current_account_id is not None:
cfg = babase.app.config cfg = babase.app.config
vals = cfg.get(self._LEAGUE_VIS_VALS_CONFIG_KEY) vals = cfg.get(self._ACCOUNT_STATE_CONFIG_KEY)
if isinstance(vals, dict):
valsaccount = vals.get('a') if not isinstance(vals, dict):
return
# If the state applies to someone else, skip it.
accountid = vals.get('a')
if ( if (
isinstance(valsaccount, str) not isinstance(accountid, str)
and valsaccount == self._current_account_id or accountid != self._current_account_id
): ):
_baclassic.set_account_display_state(vals) return
purchases = vals.get('p')
if isinstance(purchases, list):
if not all(isinstance(p, str) for p in purchases):
babase.balog.exception('Invalid purchases state on restore.')
else:
self._current_purchases = frozenset(purchases)
self._current_purchases_state = self._state_from_purchases(
purchases
)
babase.app.classic.purchases = self._current_purchases
_baclassic.set_account_state(vals)

View file

@ -1,5 +1,7 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
# pylint: disable=too-many-lines
"""Provides classic app subsystem.""" """Provides classic app subsystem."""
from __future__ import annotations from __future__ import annotations
@ -16,7 +18,6 @@ import bascenev1
import _baclassic import _baclassic
from baclassic._music import MusicSubsystem from baclassic._music import MusicSubsystem
from baclassic._accountv1 import AccountV1Subsystem from baclassic._accountv1 import AccountV1Subsystem
from baclassic._ads import AdsSubsystem
from baclassic._net import MasterServerResponseType, MasterServerV1CallThread from baclassic._net import MasterServerResponseType, MasterServerV1CallThread
from baclassic._achievement import AchievementSubsystem from baclassic._achievement import AchievementSubsystem
from baclassic._tips import get_all_tips from baclassic._tips import get_all_tips
@ -52,7 +53,6 @@ class ClassicAppSubsystem(babase.AppSubsystem):
self._env = babase.env() self._env = babase.env()
self.accounts = AccountV1Subsystem() self.accounts = AccountV1Subsystem()
self.ads = AdsSubsystem()
self.ach = AchievementSubsystem() self.ach = AchievementSubsystem()
self.store = StoreSubsystem() self.store = StoreSubsystem()
self.music = MusicSubsystem() self.music = MusicSubsystem()
@ -77,11 +77,12 @@ class ClassicAppSubsystem(babase.AppSubsystem):
# Classic-specific account state. # Classic-specific account state.
self.remove_ads = False self.remove_ads = False
self.gold_pass = False self.gold_pass = False
self.tickets = 0
self.tokens = 0 self.tokens = 0
self.chest_dock_full = False self.chest_dock_full = False
self.purchases: frozenset[str] = frozenset()
# Main Menu. # Main Menu.
self.main_menu_did_initial_transition = False
self.main_menu_last_news_fetch_time: float | None = None self.main_menu_last_news_fetch_time: float | None = None
# Spaz. # Spaz.
@ -135,6 +136,31 @@ class ClassicAppSubsystem(babase.AppSubsystem):
else: else:
self.main_menu_resume_callbacks.append(call) self.main_menu_resume_callbacks.append(call)
def can_show_interstitial(self) -> bool:
"""Is this an appropriate time for an interstitial ad?"""
# Pro or other upgrades disable interstitials.
if self.accounts.have_pro() or self.gold_pass or self.remove_ads:
return False
# Don't show ads during tournaments.
#
# UPDATE: Actually gonna leave this on. Previously it made no
# sense because ads were used to *enter* tournaments, but now
# that they are free it seems like we shouldn't give tourney
# play an advantage over other co-op play.
if bool(False):
try:
session = bascenev1.get_foreground_host_session()
assert session is not None
is_tournament = session.tournament_id is not None
except Exception:
is_tournament = False
if is_tournament:
return False
return True
@property @property
def platform(self) -> str: def platform(self) -> str:
"""Name of the current platform. """Name of the current platform.
@ -178,9 +204,12 @@ class ClassicAppSubsystem(babase.AppSubsystem):
self.music.on_app_loading() self.music.on_app_loading()
# Non-test, non-debug builds should generally be blessed; warn # Non-test, non-debug builds should generally be blessed; warn
# if not (so I don't accidentally release a build that can't # if not (so I don't accidentally release one).
# play tourneys). if (
if not env.debug and not env.test and not plus.is_blessed(): not env.debug_build
and not env.variant is type(env.variant).TEST_BUILD
and not plus.is_blessed()
):
babase.screenmessage('WARNING: NON-BLESSED BUILD', color=(1, 0, 0)) babase.screenmessage('WARNING: NON-BLESSED BUILD', color=(1, 0, 0))
stdmaps.register_all_maps() stdmaps.register_all_maps()
@ -473,15 +502,13 @@ class ClassicAppSubsystem(babase.AppSubsystem):
request: str, request: str,
data: dict[str, Any], data: dict[str, Any],
callback: MasterServerCallback | None = None, callback: MasterServerCallback | None = None,
response_type: MasterServerResponseType = MasterServerResponseType.JSON,
) -> None: ) -> None:
"""Make a call to the master server via a http GET. """Make a call to the master server via a http GET.
:meta private: :meta private:
""" """
MasterServerV1CallThread( MasterServerV1CallThread(
request, 'get', data, callback, response_type request, 'get', data, callback, MasterServerResponseType.JSON
).start() ).start()
def master_server_v1_post( def master_server_v1_post(
@ -489,14 +516,13 @@ class ClassicAppSubsystem(babase.AppSubsystem):
request: str, request: str,
data: dict[str, Any], data: dict[str, Any],
callback: MasterServerCallback | None = None, callback: MasterServerCallback | None = None,
response_type: MasterServerResponseType = MasterServerResponseType.JSON,
) -> None: ) -> None:
"""Make a call to the master server via a http POST. """Make a call to the master server via a http POST.
:meta private: :meta private:
""" """
MasterServerV1CallThread( MasterServerV1CallThread(
request, 'post', data, callback, response_type request, 'post', data, callback, MasterServerResponseType.JSON
).start() ).start()
def set_tournament_prize_image( def set_tournament_prize_image(
@ -658,12 +684,6 @@ class ClassicAppSubsystem(babase.AppSubsystem):
V2UpgradeWindow(login_name, code) V2UpgradeWindow(login_name, code)
def account_link_code_window(self, data: dict[str, Any]) -> None:
"""(internal)"""
from bauiv1lib.account.link import AccountLinkCodeWindow
AccountLinkCodeWindow(data)
def server_dialog(self, delay: float, data: dict[str, Any]) -> None: def server_dialog(self, delay: float, data: dict[str, Any]) -> None:
"""(internal)""" """(internal)"""
from bauiv1lib.serverdialog import ( from bauiv1lib.serverdialog import (
@ -788,27 +808,29 @@ class ClassicAppSubsystem(babase.AppSubsystem):
else: else:
self.party_window = weakref.ref(PartyWindow(origin=origin)) self.party_window = weakref.ref(PartyWindow(origin=origin))
def device_menu_press(self, device_id: int | None) -> None: def request_main_ui(self) -> None:
"""(internal)""" """(internal)"""
from bauiv1lib.ingamemenu import InGameMenuWindow from bauiv1lib.ingamemenu import InGameMenuWindow
from bauiv1 import set_ui_input_device
assert babase.app is not None assert babase.app is not None
in_main_menu = babase.app.ui_v1.has_main_window() if not babase.app.ui_v1.has_main_window():
if not in_main_menu:
set_ui_input_device(device_id)
# Hack(ish). We play swish sound here so it happens for # Note: we play a swish here for when our UI comes in, so we
# device presses, but this means we need to disable default # need to make sure to disable swish sounds for any buttons
# swish sounds for any menu buttons or we'll get double. # that lead us here.
if babase.app.env.gui: if babase.app.env.gui:
bauiv1.getsound('swish').play() bauiv1.getsound('swish').play()
# Pause gameplay. # Pause gameplay.
self.pause() self.pause()
menu_button = bauiv1.get_special_widget('menu_button')
babase.app.ui_v1.set_main_window( babase.app.ui_v1.set_main_window(
InGameMenuWindow(), is_top_level=True, suppress_warning=True InGameMenuWindow(
transition='scale_in', origin_widget=menu_button
),
is_top_level=True,
suppress_warning=True,
) )
def save_ui_state(self) -> None: def save_ui_state(self) -> None:
@ -826,7 +848,11 @@ class ClassicAppSubsystem(babase.AppSubsystem):
# Bring up the last place we were, or start at the main menu # Bring up the last place we were, or start at the main menu
# otherwise. # otherwise.
app = bauiv1.app app = bauiv1.app
env = app.env
variant = babase.app.env.variant
vart = type(variant)
arcade_or_demo = variant is vart.ARCADE or variant is vart.DEMO
with bascenev1.ContextRef.empty(): with bascenev1.ContextRef.empty():
assert app.classic is not None assert app.classic is not None
@ -837,7 +863,7 @@ class ClassicAppSubsystem(babase.AppSubsystem):
# When coming back from a kiosk-mode game, jump to the # When coming back from a kiosk-mode game, jump to the
# kiosk start screen. # kiosk start screen.
if env.demo or env.arcade: if arcade_or_demo:
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
from bauiv1lib.kiosk import KioskWindow from bauiv1lib.kiosk import KioskWindow
@ -963,14 +989,14 @@ class ClassicAppSubsystem(babase.AppSubsystem):
def is_game_unlocked(self, game: str) -> bool: def is_game_unlocked(self, game: str) -> bool:
"""Is a particular game unlocked?""" """Is a particular game unlocked?"""
plus = babase.app.plus classic = babase.app.classic
assert plus is not None assert classic is not None
purchases = self.required_purchases_for_game(game) purchases = self.required_purchases_for_game(game)
if not purchases: if not purchases:
return True return True
for purchase in purchases: for purchase in purchases:
if not plus.get_v1_account_product_purchased(purchase): if not purchase in classic.purchases:
return False return False
return True return True

View file

@ -36,3 +36,13 @@ def on_engine_did_reset() -> None:
logging.error( logging.error(
'on_engine_did_reset called without ClassicAppMode active.' 'on_engine_did_reset called without ClassicAppMode active.'
) )
def request_main_ui() -> None:
"""Called to bring up in-game menu."""
if babase.app.classic is None:
logging.exception('Classic not present.')
return
babase.app.classic.request_main_ui()

View file

@ -3,12 +3,18 @@
"""Networking related functionality.""" """Networking related functionality."""
from __future__ import annotations from __future__ import annotations
import zlib
import copy import copy
import time
import base64
import weakref import weakref
import threading import threading
from enum import Enum from enum import Enum
from typing import TYPE_CHECKING, override from typing import TYPE_CHECKING, override
from efro.error import CommunicationError
from efro.util import strip_exception_tracebacks
import bacommon.bs
import babase import babase
import bascenev1 import bascenev1
@ -37,9 +43,7 @@ class MasterServerV1CallThread(threading.Thread):
): ):
# pylint: disable=too-many-positional-arguments # pylint: disable=too-many-positional-arguments
# Set daemon=True so long-running requests don't keep us from super().__init__()
# quitting the app.
super().__init__(daemon=True)
self._request = request self._request = request
self._request_type = request_type self._request_type = request_type
if not isinstance(response_type, MasterServerResponseType): if not isinstance(response_type, MasterServerResponseType):
@ -49,9 +53,16 @@ class MasterServerV1CallThread(threading.Thread):
self._callback: MasterServerCallback | None = callback self._callback: MasterServerCallback | None = callback
self._context = babase.ContextRef() self._context = babase.ContextRef()
appstate = babase.app.state
if appstate.value < type(appstate).LOADING.value:
raise RuntimeError(
'Cannot use MasterServerV1CallThread'
' until app reaches LOADING state.'
)
# Save and restore the context we were created from. # Save and restore the context we were created from.
activity = bascenev1.getactivity(doraise=False) activity = bascenev1.getactivity(doraise=False)
self._activity = weakref.ref(activity) if activity is not None else None self._activity = None if activity is None else weakref.ref(activity)
def _run_callback(self, arg: None | dict[str, Any]) -> None: def _run_callback(self, arg: None | dict[str, Any]) -> None:
# If we were created in an activity context and that activity # If we were created in an activity context and that activity
@ -72,23 +83,24 @@ class MasterServerV1CallThread(threading.Thread):
self._callback(arg) self._callback(arg)
@override @override
def run(self) -> None: def __str__(self) -> str:
# pylint: disable=consider-using-with return (
# pylint: disable=too-many-branches f'<MasterServerV1CallThread id={id(self)}'
import urllib.request f' request={self._request}>'
import urllib.parse )
import urllib.error
import json
from efro.error import is_urllib_communication_error @override
def run(self) -> None:
import urllib.parse
import json
plus = babase.app.plus plus = babase.app.plus
assert plus is not None assert plus is not None
response_data: Any = None response_data: Any = None
url: str | None = None
# Tearing the app down while this is running can lead to starttime = time.monotonic()
# rare crashes in LibSSL, so avoid that if at all possible.
# Disallow shutdown while we're working.
if not babase.shutdown_suppress_begin(): if not babase.shutdown_suppress_begin():
# App is already shutting down, so we're a no-op. # App is already shutting down, so we're a no-op.
return return
@ -98,67 +110,62 @@ class MasterServerV1CallThread(threading.Thread):
assert classic is not None assert classic is not None
self._data = _utf8_all(self._data) self._data = _utf8_all(self._data)
babase.set_thread_name('BA_ServerCallThread') babase.set_thread_name('BA_ServerCallThread')
if self._request_type == 'get':
msaddr = plus.get_master_server_address()
dataenc = urllib.parse.urlencode(self._data) dataenc = urllib.parse.urlencode(self._data)
url = f'{msaddr}/{self._request}?{dataenc}'
assert url is not None mresponse = plus.cloud.send_message(
response = urllib.request.urlopen( bacommon.bs.LegacyRequest(
urllib.request.Request( self._request,
url, self._request_type,
None, classic.legacy_user_agent_string,
{'User-Agent': classic.legacy_user_agent_string}, dataenc,
),
context=babase.app.net.sslcontext,
timeout=babase.DEFAULT_REQUEST_TIMEOUT_SECONDS,
) )
elif self._request_type == 'post':
url = f'{plus.get_master_server_address()}/{self._request}'
assert url is not None
response = urllib.request.urlopen(
urllib.request.Request(
url,
urllib.parse.urlencode(self._data).encode(),
{'User-Agent': classic.legacy_user_agent_string},
),
context=babase.app.net.sslcontext,
timeout=babase.DEFAULT_REQUEST_TIMEOUT_SECONDS,
) )
mrdata: str | None
if mresponse.data is None:
mrdata = None
elif mresponse.zipped:
mrdata = zlib.decompress(
base64.b85decode(mresponse.data)
).decode()
else: else:
raise TypeError('Invalid request_type: ' + self._request_type) mrdata = mresponse.data
# If html request failed. if mrdata is None:
if response.getcode() != 200:
response_data = None
elif self._response_type == MasterServerResponseType.JSON:
raw_data = response.read()
# Empty string here means something failed server side.
if raw_data == b'':
response_data = None response_data = None
else: else:
response_data = json.loads(raw_data) assert self._response_type == MasterServerResponseType.JSON
else: response_data = json.loads(mrdata)
raise TypeError(f'invalid responsetype: {self._response_type}')
except Exception as exc: except Exception as exc:
duration = time.monotonic() - starttime
# Ignore common network errors; note unexpected ones. # Ignore common network errors; note unexpected ones.
if not is_urllib_communication_error(exc, url=url): if isinstance(exc, CommunicationError):
print( babase.netlog.debug(
f'Error in MasterServerCallThread' 'Legacy %s request failed in %.3fs (communication error).',
f' (url={url},' self._request,
f' response-type={self._response_type},' duration,
f' response-data={response_data}):' )
else:
babase.netlog.exception(
'Legacy %s request failed in %.3fs.',
self._request,
duration,
) )
import traceback
traceback.print_exc()
response_data = None response_data = None
# We're done with the exception, so strip its tracebacks to
# avoid reference cycles.
strip_exception_tracebacks(exc)
finally: finally:
babase.shutdown_suppress_end() babase.shutdown_suppress_end()
if response_data is not None:
duration = time.monotonic() - starttime
babase.netlog.debug(
'Legacy %s request succeeded in %.3fs.', self._request, duration
)
if self._callback is not None: if self._callback is not None:
babase.pushcall( babase.pushcall(
babase.Call(self._run_callback, response_data), babase.Call(self._run_callback, response_data),
@ -167,7 +174,7 @@ class MasterServerV1CallThread(threading.Thread):
def _utf8_all(data: Any) -> Any: def _utf8_all(data: Any) -> Any:
"""Convert any unicode data in provided sequence(s) to utf8 bytes.""" """Convert all strings in provided data to utf-8 bytes."""
if isinstance(data, dict): if isinstance(data, dict):
return dict( return dict(
(_utf8_all(key), _utf8_all(value)) (_utf8_all(key), _utf8_all(value))
@ -178,5 +185,6 @@ def _utf8_all(data: Any) -> Any:
if isinstance(data, tuple): if isinstance(data, tuple):
return tuple(_utf8_all(element) for element in data) return tuple(_utf8_all(element) for element in data)
if isinstance(data, str): if isinstance(data, str):
return data.encode('utf-8', errors='ignore') # return data.encode('utf-8', errors='ignore')
return data.encode()
return data return data

View file

@ -95,6 +95,7 @@ class ServerController:
self._ran_access_check = False self._ran_access_check = False
self._prep_timer: babase.AppTimer | None = None self._prep_timer: babase.AppTimer | None = None
self._next_stuck_login_warn_time = time.time() + 10.0 self._next_stuck_login_warn_time = time.time() + 10.0
self._next_connectivity_warn_time = time.time() + 5.0
self._first_run = True self._first_run = True
self._shutdown_reason: ShutdownReason | None = None self._shutdown_reason: ShutdownReason | None = None
self._executing_shutdown = False self._executing_shutdown = False
@ -255,6 +256,18 @@ class ServerController:
"""Run in a timer to do prep before beginning to serve.""" """Run in a timer to do prep before beginning to serve."""
plus = babase.app.plus plus = babase.app.plus
assert plus is not None assert plus is not None
# Cloud connectivity is a prerequisite (v1 comms goes through this now).
if not plus.cloud.connected:
# Bringing up a cloud connection should not take long;
# complain if it does.
curtime = time.time()
if curtime > self._next_connectivity_warn_time:
print('Still waiting for cloud connectivity...')
self._next_connectivity_warn_time = curtime + 5.0
return
# Being signed in via v1 is a prerequisite.
signed_in = plus.get_v1_account_state() == 'signed_in' signed_in = plus.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;

View file

@ -437,13 +437,14 @@ class StoreSubsystem:
def get_available_purchase_count(self, tab: str | None = None) -> int: def get_available_purchase_count(self, tab: str | None = None) -> int:
"""(internal)""" """(internal)"""
plus = babase.app.plus plus = babase.app.plus
if plus is None: classic = babase.app.classic
if plus is None or classic is None:
return 0 return 0
try: try:
if plus.get_v1_account_state() != 'signed_in': if plus.accounts.primary is None:
return 0 return 0
count = 0 count = 0
our_tickets = plus.get_v1_account_ticket_count() our_tickets = classic.tickets
store_data = self.get_store_layout() store_data = self.get_store_layout()
if tab is not None: if tab is not None:
tabs = [(tab, store_data[tab])] tabs = [(tab, store_data[tab])]
@ -463,22 +464,22 @@ class StoreSubsystem:
) -> int: ) -> int:
plus = babase.app.plus plus = babase.app.plus
assert plus assert plus
assert babase.app.classic is not None
purchases = babase.app.classic.purchases
for section in tabval: for section in tabval:
for item in section['items']: for item in section['items']:
ticket_cost = plus.get_v1_account_misc_read_val( ticket_cost = plus.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 ( if our_tickets >= ticket_cost and item not in purchases:
our_tickets >= ticket_cost
and not plus.get_v1_account_product_purchased(item)
):
count += 1 count += 1
return count return count
def get_available_sale_time(self, tab: str) -> int | None: def get_available_sale_time(self, tab: str) -> int | None:
"""(internal)""" """(internal)"""
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
# pylint: disable=too-many-locals
# pylint: disable=too-many-nested-blocks # pylint: disable=too-many-nested-blocks
plus = babase.app.plus plus = babase.app.plus
assert plus is not None assert plus is not None
@ -488,6 +489,7 @@ class StoreSubsystem:
app = babase.app app = babase.app
assert app.classic is not None assert app.classic is not None
purchases = app.classic.purchases
sale_times: list[int | None] = [] sale_times: list[int | None] = []
# Calc time for our pro sale (old special case). # Calc time for our pro sale (old special case).
@ -546,7 +548,7 @@ class StoreSubsystem:
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 plus.get_v1_account_product_purchased(item): if item not in purchases:
to_end = ( to_end = (
datetime.datetime.fromtimestamp( datetime.datetime.fromtimestamp(
sales_raw[item]['e'], datetime.UTC sales_raw[item]['e'], datetime.UTC
@ -566,15 +568,13 @@ class StoreSubsystem:
def get_unowned_maps(self) -> list[str]: def get_unowned_maps(self) -> list[str]:
"""Return the list of local maps not owned by the current account.""" """Return the list of local maps not owned by the current account."""
plus = babase.app.plus classic = babase.app.classic
purchases = classic.purchases if classic is not None else set()
unowned_maps: set[str] = set() unowned_maps: set[str] = set()
if babase.app.env.gui: if babase.app.env.gui:
for map_section in self.get_store_layout()['maps']: for map_section in self.get_store_layout()['maps']:
for mapitem in map_section['items']: for mapitem in map_section['items']:
if ( if mapitem not in purchases:
plus is None
or not plus.get_v1_account_product_purchased(mapitem)
):
m_info = self.get_store_item(mapitem) m_info = self.get_store_item(mapitem)
unowned_maps.add(m_info['map_type'].name) unowned_maps.add(m_info['map_type'].name)
return sorted(unowned_maps) return sorted(unowned_maps)
@ -582,7 +582,8 @@ class StoreSubsystem:
def get_unowned_game_types(self) -> set[type[bascenev1.GameActivity]]: def get_unowned_game_types(self) -> set[type[bascenev1.GameActivity]]:
"""Return present game types not owned by the current account.""" """Return present game types not owned by the current account."""
try: try:
plus = babase.app.plus classic = babase.app.classic
purchases = classic.purchases if classic is not None else set()
unowned_games: set[type[bascenev1.GameActivity]] = set() unowned_games: set[type[bascenev1.GameActivity]] = set()
if babase.app.env.gui: if babase.app.env.gui:
for section in self.get_store_layout()['minigames']: for section in self.get_store_layout()['minigames']:
@ -591,10 +592,7 @@ class StoreSubsystem:
# Ignore things like infinite onslaught which # Ignore things like infinite onslaught which
# aren't actually game types. # aren't actually game types.
continue continue
if ( if mname not in purchases:
plus is None
or not plus.get_v1_account_product_purchased(mname)
):
m_info = self.get_store_item(mname) m_info = self.get_store_item(mname)
unowned_games.add(m_info['gametype']) unowned_games.add(m_info['gametype'])
return unowned_games return unowned_games

View file

@ -110,6 +110,7 @@ def get_all_tips() -> list[str]:
'If your framerate is choppy, try turning down resolution\nor ' 'If your framerate is choppy, try turning down resolution\nor '
'visuals in the game\'s graphics settings.' 'visuals in the game\'s graphics settings.'
] ]
if ( if (
app.classic is not None app.classic is not None
and app.classic.platform in ('android', 'ios') and app.classic.platform in ('android', 'ios')

View file

@ -116,12 +116,12 @@ class AppPlatform(Enum):
class AppVariant(Enum): class AppVariant(Enum):
"""A unique Ballistica build type within a single platform. """A unique Ballistica build variation within a single platform.
Each distinct flavor of an app has a unique combination of Each distinct permutation of an app has a unique combination of
AppPlatform and AppVariant. Generally platform describes a set of :class:`AppPlatform` and ``AppVariant``. Generally platform
hardware, while variant describes a destination or purpose for the describes a set of hardware, while variant describes a destination
build. or purpose for the build.
""" """
#: Default builds. #: Default builds.

View file

@ -120,8 +120,8 @@ class ResponseData:
tuple[list[str], str, dict] | None, IOAttrs('u', store_default=False) tuple[list[str], str, dict] | None, IOAttrs('u', store_default=False)
] = None ] = None
#: If present, a list of pathnames that should be gzipped #: If present, a list of pathnames that should be gzipped and
#: and uploaded to an 'uploads_inline' bytes dict in end_command args. #: uploaded to an 'uploads_inline' bytes dict in end_command args.
#: This should be limited to relatively small files. #: This should be limited to relatively small files.
uploads_inline: Annotated[ uploads_inline: Annotated[
list[str] | None, IOAttrs('uinl', store_default=False) list[str] | None, IOAttrs('uinl', store_default=False)
@ -138,9 +138,9 @@ class ResponseData:
Downloads | None, IOAttrs('dl', store_default=False) Downloads | None, IOAttrs('dl', store_default=False)
] = None ] = None
#: If present, pathnames mapped to gzipped data to #: If present, pathnames mapped to gzipped data to be written to the
#: be written to the client. This should only be used for relatively #: client. This should only be used for relatively small files as
#: small files as they are all included inline as part of the response. #: they are all included inline as part of the response.
downloads_inline: Annotated[ downloads_inline: Annotated[
dict[str, bytes] | None, IOAttrs('dinl', store_default=False) dict[str, bytes] | None, IOAttrs('dinl', store_default=False)
] = None ] = None
@ -153,10 +153,10 @@ class ResponseData:
#: If present, url to display to the user. #: If present, url to display to the user.
open_url: Annotated[str | None, IOAttrs('url', store_default=False)] = None open_url: Annotated[str | None, IOAttrs('url', store_default=False)] = None
#: If present, a line of input is read and placed into #: If present, a line of input is read and placed into end_command
#: end_command args as 'input'. The first value is the prompt printed #: args as 'input'. The first value is the prompt printed before
#: before reading and the second is whether it should be read as a #: reading and the second is whether it should be read as a password
#: password (without echoing to the terminal). #: (without echoing to the terminal).
input_prompt: Annotated[ input_prompt: Annotated[
tuple[str, bool] | None, IOAttrs('inp', store_default=False) tuple[str, bool] | None, IOAttrs('inp', store_default=False)
] = None ] = None
@ -170,8 +170,8 @@ class ResponseData:
#: End arg for end_message print() call. #: End arg for end_message print() call.
end_message_end: Annotated[str, IOAttrs('eme', store_default=False)] = '\n' end_message_end: Annotated[str, IOAttrs('eme', store_default=False)] = '\n'
#: If present, this command is run with these args at the end #: If present, this command is run with these args at the end of
#: of response processing. #: response processing.
end_command: Annotated[ end_command: Annotated[
tuple[str, dict] | None, IOAttrs('ec', store_default=False) tuple[str, dict] | None, IOAttrs('ec', store_default=False)
] = None ] = None

View file

@ -1,5 +1,6 @@
# Released under the MIT License. See LICENSE for details. # Released under the MIT License. See LICENSE for details.
# #
# pylint: disable=too-many-lines
"""BombSquad specific bits.""" """BombSquad specific bits."""
from __future__ import annotations from __future__ import annotations
@ -20,6 +21,31 @@ TOKENS3_COUNT = 1200
TOKENS4_COUNT = 2600 TOKENS4_COUNT = 2600
@ioprepped
@dataclass
class LegacyRequest(Message):
"""A generic request for the legacy master server."""
request: Annotated[str, IOAttrs('r')]
request_type: Annotated[str, IOAttrs('t')]
user_agent_string: Annotated[str, IOAttrs('u')]
data: Annotated[str, IOAttrs('d')]
@override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [LegacyResponse]
@ioprepped
@dataclass
class LegacyResponse(Response):
"""Response for generic legacy request."""
data: Annotated[str | None, IOAttrs('d')]
zipped: Annotated[bool, IOAttrs('z')]
@ioprepped @ioprepped
@dataclass @dataclass
class PrivatePartyMessage(Message): class PrivatePartyMessage(Message):
@ -44,6 +70,25 @@ class PrivatePartyResponse(Response):
datacode: Annotated[str | None, IOAttrs('d')] datacode: Annotated[str | None, IOAttrs('d')]
@ioprepped
@dataclass
class GetClassicPurchasesMessage(Message):
"""Asking for current account's classic purchases."""
@override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [GetClassicPurchasesResponse]
@ioprepped
@dataclass
class GetClassicPurchasesResponse(Response):
"""Here's those classic purchases ya asked for boss."""
purchases: Annotated[set[str], IOAttrs('p')]
class ClassicChestAppearance(Enum): class ClassicChestAppearance(Enum):
"""Appearances bombsquad classic chests can have.""" """Appearances bombsquad classic chests can have."""
@ -108,6 +153,11 @@ class ClassicAccountLiveData:
GOLD = 'g' GOLD = 'g'
DIAMOND = 'd' DIAMOND = 'd'
class Flag(Enum):
"""Flags set for our account."""
ASK_FOR_REVIEW = 'r'
tickets: Annotated[int, IOAttrs('ti')] tickets: Annotated[int, IOAttrs('ti')]
tokens: Annotated[int, IOAttrs('to')] tokens: Annotated[int, IOAttrs('to')]
@ -131,6 +181,11 @@ class ClassicAccountLiveData:
chests: Annotated[dict[str, Chest], IOAttrs('c')] chests: Annotated[dict[str, Chest], IOAttrs('c')]
# State id of our purchases for builds 22459+.
purchases_state: Annotated[str | None, IOAttrs('p')]
flags: Annotated[set[Flag], IOAttrs('f', soft_default_factory=set)]
class DisplayItemTypeID(Enum): class DisplayItemTypeID(Enum):
"""Type ID for each of our subclasses.""" """Type ID for each of our subclasses."""
@ -738,7 +793,7 @@ class ClientEffectScreenMessage(ClientEffect):
"""Display a screen-message.""" """Display a screen-message."""
message: Annotated[str, IOAttrs('m')] message: Annotated[str, IOAttrs('m')]
subs: Annotated[list[str], IOAttrs('s')] subs: Annotated[list[str], IOAttrs('s')] = field(default_factory=list)
color: Annotated[tuple[float, float, float], IOAttrs('c')] = (1.0, 1.0, 1.0) color: Annotated[tuple[float, float, float], IOAttrs('c')] = (1.0, 1.0, 1.0)
@override @override
@ -957,3 +1012,51 @@ class ChestActionResponse(Response):
effects: Annotated[ effects: Annotated[
list[ClientEffect], IOAttrs('fx', store_default=False) list[ClientEffect], IOAttrs('fx', store_default=False)
] = field(default_factory=list) ] = field(default_factory=list)
@ioprepped
@dataclass
class GlobalProfileCheckMessage(Message):
"""Is this global profile name available?"""
name: Annotated[str, IOAttrs('n')]
@override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [GlobalProfileCheckResponse]
@ioprepped
@dataclass
class GlobalProfileCheckResponse(Response):
"""Here's that profile check ya asked for boss."""
available: Annotated[bool, IOAttrs('a')]
ticket_cost: Annotated[int, IOAttrs('tc')]
@ioprepped
@dataclass
class SendInfoMessage(Message):
"""User is using the send-info function."""
description: Annotated[str, IOAttrs('c')]
@override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [SendInfoResponse]
@ioprepped
@dataclass
class SendInfoResponse(Response):
"""Response to sending info to the server."""
handled: Annotated[bool, IOAttrs('v')]
message: Annotated[str | None, IOAttrs('m', store_default=False)] = None
effects: Annotated[
list[ClientEffect], IOAttrs('e', store_default=False)
] = field(default_factory=list)
legacy_code: Annotated[str | None, IOAttrs('l', store_default=False)] = None

View file

@ -25,6 +25,24 @@ class WebLocation(Enum):
ACCOUNT_DELETE_SECTION = 'd' ACCOUNT_DELETE_SECTION = 'd'
@ioprepped
@dataclass
class CloudVals:
"""Engine config values provided by the master server.
Used to convey things such as debug logging.
"""
#: Fully qualified type names we should emit extra debug logs for
#: when garbage-collected (for debugging ref loops).
gc_debug_types: Annotated[
list[str], IOAttrs('gct', store_default=False)
] = field(default_factory=list)
#: Max number of objects of a given type to emit debug logs for.
gc_debug_type_limit: Annotated[int, IOAttrs('gdl', store_default=False)] = 2
@ioprepped @ioprepped
@dataclass @dataclass
class LoginProxyRequestMessage(Message): class LoginProxyRequestMessage(Message):
@ -132,29 +150,6 @@ class TestResponse(Response):
testfoo: Annotated[int, IOAttrs('f')] testfoo: Annotated[int, IOAttrs('f')]
@ioprepped
@dataclass
class SendInfoMessage(Message):
"""User is using the send-info function"""
description: Annotated[str, IOAttrs('c')]
@override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [SendInfoResponse]
@ioprepped
@dataclass
class SendInfoResponse(Response):
"""Response to sending into the server."""
handled: Annotated[bool, IOAttrs('v')]
message: Annotated[str | None, IOAttrs('m', store_default=False)] = None
legacy_code: Annotated[str | None, IOAttrs('l', store_default=False)] = None
@ioprepped @ioprepped
@dataclass @dataclass
class WorkspaceFetchState: class WorkspaceFetchState:
@ -343,3 +338,22 @@ class SecureDataCheckerResponse(Response):
"""Here's that checker ya asked for, boss.""" """Here's that checker ya asked for, boss."""
checker: Annotated[SecureDataChecker, IOAttrs('c')] checker: Annotated[SecureDataChecker, IOAttrs('c')]
@ioprepped
@dataclass
class CloudValsRequest(Message):
"""Can a fella get some cloud vals around here?."""
@override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [CloudValsResponse]
@ioprepped
@dataclass
class CloudValsResponse(Response):
"""Here's them cloud vals ya asked for, boss."""
vals: Annotated[CloudVals, IOAttrs('v')]

View file

@ -18,9 +18,9 @@ class Locale(Enum):
This list of locales is considered 'sacred' - we assume any values This list of locales is considered 'sacred' - we assume any values
(and associated long values) added here remain in use out in the (and associated long values) added here remain in use out in the
wild indefinitely. If a locale value is superseded by a newer or wild indefinitely. If a locale is superseded by a newer or more
more specific one, the new value should be added and both new and specific one, the new locale should be added and both new and old
old should map to the same LocaleResolved value. should map to the same :class:`LocaleResolved`.
""" """
# Locale values are not iso codes or anything specific; just # Locale values are not iso codes or anything specific; just
@ -70,6 +70,7 @@ class Locale(Enum):
UKRAINIAN = 'ukrn' UKRAINIAN = 'ukrn'
VENETIAN = 'venetn' VENETIAN = 'venetn'
VIETNAMESE = 'viet' VIETNAMESE = 'viet'
KAZAKH = 'kazk'
# Note: We use if-statement chains here so we can use assert_never() # Note: We use if-statement chains here so we can use assert_never()
# to ensure we cover all existing values. But we cache lookups so # to ensure we cover all existing values. But we cache lookups so
@ -172,6 +173,8 @@ class Locale(Enum):
return 'Venetian' return 'Venetian'
if self is cls.VIETNAMESE: if self is cls.VIETNAMESE:
return 'Vietnamese' return 'Vietnamese'
if self is cls.KAZAKH:
return 'Kazakh'
# Make sure we've covered all cases. # Make sure we've covered all cases.
assert_never(self) assert_never(self)
@ -192,6 +195,111 @@ class Locale(Enum):
except KeyError as exc: except KeyError as exc:
raise ValueError(f'Invalid long value "{value}"') from exc raise ValueError(f'Invalid long value "{value}"') from exc
@cached_property
def description(self) -> str:
"""A human readable description for the locale.
Intended as instructions to humans or AI for translating. For
most locales this is simply the language name, but for special
ones like pirate-speak it may include instructions.
"""
# pylint: disable=too-many-branches
# pylint: disable=too-many-return-statements
cls = type(self)
if self is cls.ENGLISH:
return 'English'
if self is cls.CHINESE:
return 'Chinese'
if self is cls.CHINESE_TRADITIONAL:
return 'Chinese (Traditional)'
if self is cls.CHINESE_SIMPLIFIED:
return 'Chinese (Simplified)'
if self is cls.PORTUGUESE:
return 'Portuguese'
if self is cls.PORTUGUESE_PORTUGAL:
return 'Portuguese (Portugal)'
if self is cls.PORTUGUESE_BRAZIL:
return 'Portuguese (Brazil)'
if self is cls.ARABIC:
return 'Arabic'
if self is cls.BELARUSSIAN:
return 'Belarussian'
if self is cls.CROATIAN:
return 'Croatian'
if self is cls.CZECH:
return 'Czech'
if self is cls.DANISH:
return 'Danish'
if self is cls.DUTCH:
return 'Dutch'
if self is cls.PIRATE_SPEAK:
return 'Pirate-Speak (English as spoken by a pirate)'
if self is cls.ESPERANTO:
return 'Esperanto'
if self is cls.FILIPINO:
return 'Filipino'
if self is cls.FRENCH:
return 'French'
if self is cls.GERMAN:
return 'German'
if self is cls.GIBBERISH:
return (
'Gibberish (imaginary words vaguely' ' reminiscent of English)'
)
if self is cls.GREEK:
return 'Greek'
if self is cls.HINDI:
return 'Hindi'
if self is cls.HUNGARIAN:
return 'Hungarian'
if self is cls.INDONESIAN:
return 'Indonesian'
if self is cls.ITALIAN:
return 'Italian'
if self is cls.KOREAN:
return 'Korean'
if self is cls.MALAY:
return 'Malay'
if self is cls.PERSIAN:
return 'Persian'
if self is cls.POLISH:
return 'Polish'
if self is cls.ROMANIAN:
return 'Romanian'
if self is cls.RUSSIAN:
return 'Russian'
if self is cls.SERBIAN:
return 'Serbian'
if self is cls.SPANISH:
return 'Spanish'
if self is cls.SPANISH_LATIN_AMERICA:
return 'Spanish (Latin America)'
if self is cls.SPANISH_SPAIN:
return 'Spanish (Spain)'
if self is cls.SLOVAK:
return 'Slovak'
if self is cls.SWEDISH:
return 'Swedish'
if self is cls.TAMIL:
return 'Tamil'
if self is cls.THAI:
return 'Thai'
if self is cls.TURKISH:
return 'Turkish'
if self is cls.UKRAINIAN:
return 'Ukrainian'
if self is cls.VENETIAN:
return 'Venetian'
if self is cls.VIETNAMESE:
return 'Vietnamese'
if self is cls.KAZAKH:
return 'Kazakh'
# Make sure we've covered all cases.
assert_never(self)
@cached_property @cached_property
def resolved(self) -> LocaleResolved: def resolved(self) -> LocaleResolved:
"""Return the associated resolved locale.""" """Return the associated resolved locale."""
@ -279,6 +387,8 @@ class Locale(Enum):
return R.VENETIAN return R.VENETIAN
if self is cls.VIETNAMESE: if self is cls.VIETNAMESE:
return R.VIETNAMESE return R.VIETNAMESE
if self is cls.KAZAKH:
return R.KAZAKH
# Make sure we're covering all cases. # Make sure we're covering all cases.
assert_never(self) assert_never(self)
@ -333,6 +443,7 @@ class LocaleResolved(Enum):
UKRAINIAN = 'ukrn' UKRAINIAN = 'ukrn'
VENETIAN = 'venetn' VENETIAN = 'venetn'
VIETNAMESE = 'viet' VIETNAMESE = 'viet'
KAZAKH = 'kazk'
# Note: We use if-statement chains here so we can use assert_never() # Note: We use if-statement chains here so we can use assert_never()
# to ensure we cover all existing values. But we cache lookups so # to ensure we cover all existing values. But we cache lookups so
@ -433,6 +544,8 @@ class LocaleResolved(Enum):
return Locale.VENETIAN return Locale.VENETIAN
if self is cls.VIETNAMESE: if self is cls.VIETNAMESE:
return Locale.VIETNAMESE return Locale.VIETNAMESE
if self is cls.KAZAKH:
return Locale.KAZAKH
# Make sure we're covering all cases. # Make sure we're covering all cases.
assert_never(self) assert_never(self)
@ -532,6 +645,8 @@ class LocaleResolved(Enum):
val = 'vec' val = 'vec'
elif self is cls.VIETNAMESE: elif self is cls.VIETNAMESE:
val = 'vi' val = 'vi'
elif self is cls.KAZAKH:
val = 'kk'
else: else:
# Make sure we cover all cases. # Make sure we cover all cases.
assert_never(self) assert_never(self)
@ -627,19 +742,20 @@ class LocaleResolved(Enum):
if not extras or any( if not extras or any(
val in extras val in extras
for val in [ for val in [
'419', '419', # Latin America / Carribean region
'mx', 'mx', # Mexico
'ar', 'ar', # Argentina
'co', 'co', # Colombia
'cl', 'cl', # Chile
'pe', 'pe', # Peru
've', 've', # Venezuela
'cr', 'cr', # Costa Rica
'pr', 'pr', # Puerto Rico
'do', 'do', # Dominican Republic
'uy', 'uy', # Uruguay
'ec', 'ec', # Ecuador
'pa', 'pa', # Panama
'bo', # Bolivia
] ]
): ):
return cls.SPANISH_LATIN_AMERICA return cls.SPANISH_LATIN_AMERICA
@ -656,6 +772,10 @@ class LocaleResolved(Enum):
fallback.name, fallback.name,
) )
return fallback return fallback
if lang == 'c':
# The C.UTF-8 is a minimal locale defined by POSIX we
# sometimes run into.
return cls.ENGLISH
if lang == 'ar': if lang == 'ar':
return cls.ARABIC return cls.ARABIC
if lang == 'be': if lang == 'be':
@ -716,6 +836,8 @@ class LocaleResolved(Enum):
return cls.VENETIAN return cls.VENETIAN
if lang == 'vi': if lang == 'vi':
return cls.VIETNAMESE return cls.VIETNAMESE
if lang == 'kk':
return cls.KAZAKH
# Make noise if we come across something unexpected so we can # Make noise if we come across something unexpected so we can
# add it. # add it.

View file

@ -5,15 +5,92 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import TYPE_CHECKING from enum import Enum
from typing import TYPE_CHECKING, assert_never
from bacommon.loggercontrol import LoggerControlConfig from bacommon.loggercontrol import LoggerControlConfig
if TYPE_CHECKING: if TYPE_CHECKING:
pass pass
# IMPORTANT: If making any changes here, be sure to update
# binding_core.py and baenv.py where some of these same values are
# hard-coded at engine init (when they don't yet have access to this
# module).
class ClientLoggerName(Enum):
"""Logger names used on the Ballistica client."""
BA = 'ba'
ENV = 'ba.env'
APP = 'ba.app'
ASSETS = 'ba.assets'
AUDIO = 'ba.audio'
CACHE = 'ba.cache'
DISPLAYTIME = 'ba.displaytime'
GARBAGE_COLLECTION = 'ba.gc'
GRAPHICS = 'ba.gfx'
PERFORMANCE = 'ba.perf'
INPUT = 'ba.input'
LIFECYCLE = 'ba.lifecycle'
NETWORKING = 'ba.net'
CONNECTIVITY = 'ba.connectivity'
V2TRANSPORT = 'ba.v2transport'
CLOUD_SUBSCRIPTION = 'ba.cloudsub'
ACCOUNT_CLIENT_V2 = 'ba.accountclientv2'
ACCOUNT = 'ba.account'
LOGIN_ADAPTER = 'ba.loginadapter'
@property
def description(self) -> str:
"""Return a short description for the logger."""
# pylint: disable=too-many-return-statements
# pylint: disable=too-many-branches
cls = type(self)
if self is cls.BA:
return 'top level Ballistica logger - use to adjust everything'
if self is cls.ENV:
return 'engine environment bootstrapping'
if self is cls.APP:
return 'general app operation - INFO is visible by default'
if self is cls.ASSETS:
return 'textures, sounds, models, etc.'
if self is cls.AUDIO:
return 'sound and music playback'
if self is cls.CACHE:
return 'cache dir - holds pycache, assets, etc.'
if self is cls.DISPLAYTIME:
return 'timing for smooth animation display'
if self is cls.GARBAGE_COLLECTION:
return 'garbage collection - debug memory leaks/etc.'
if self is cls.GRAPHICS:
return 'anything graphics related'
if self is cls.PERFORMANCE:
return 'debug rendering speed, hitches, etc.'
if self is cls.INPUT:
return 'keyboards, touchscreens, game-controllers, etc.'
if self is cls.LIFECYCLE:
return 'bootstrapping, pausing, resuming, shutdown, etc.'
if self is cls.NETWORKING:
return 'anything network related'
if self is cls.CONNECTIVITY:
return 'determining nearest/best regional servers'
if self is cls.V2TRANSPORT:
return 'persistent connections to regional servers'
if self is cls.CLOUD_SUBSCRIPTION:
return 'live values fed from regional server'
if self is cls.ACCOUNT_CLIENT_V2:
return 'server communication for v2 accounts'
if self is cls.ACCOUNT:
return 'account functionality'
if self is cls.LOGIN_ADAPTER:
return 'support for particular login types'
assert_never(self)
def get_base_logger_control_config_client() -> LoggerControlConfig: def get_base_logger_control_config_client() -> LoggerControlConfig:
"""Return the logger-control-config used by the ballistica client. """Return the logger-control-config used by the Ballistica client.
This should remain consistent since local logger configurations This should remain consistent since local logger configurations
are stored relative to this. are stored relative to this.
@ -23,5 +100,8 @@ def get_base_logger_control_config_client() -> LoggerControlConfig:
# clean but show INFO for ba.app to get basic app startup messages # clean but show INFO for ba.app to get basic app startup messages
# and whatnot. # and whatnot.
return LoggerControlConfig( return LoggerControlConfig(
levels={'root': logging.WARNING, 'ba.app': logging.INFO} levels={
'root': logging.WARNING,
ClientLoggerName.APP.value: logging.INFO,
}
) )

View file

@ -20,11 +20,7 @@ class ServerNodeEntry:
"""Information about a specific server.""" """Information about a specific server."""
zone: Annotated[str, IOAttrs('r')] zone: Annotated[str, IOAttrs('r')]
latlong: Annotated[tuple[float, float] | None, IOAttrs('ll')]
# TODO: Remove soft_default after all master-servers upgraded.
latlong: Annotated[
tuple[float, float] | None, IOAttrs('ll', soft_default=None)
]
address: Annotated[str, IOAttrs('a')] address: Annotated[str, IOAttrs('a')]
port: Annotated[int, IOAttrs('p')] port: Annotated[int, IOAttrs('p')]
@ -43,6 +39,9 @@ class ServerNodeQueryResponse:
ping_per_dist: Annotated[float, IOAttrs('ppd')] ping_per_dist: Annotated[float, IOAttrs('ppd')]
max_dist: Annotated[float, IOAttrs('md')] max_dist: Annotated[float, IOAttrs('md')]
# If this came from a bootstrap server, which zone was it in.
bootstrap_zone: Annotated[str | None, IOAttrs('b', soft_default=None)]
debug_log_seconds: Annotated[ debug_log_seconds: Annotated[
float | None, IOAttrs('d', store_default=False) float | None, IOAttrs('d', store_default=False)
] = None ] = None

View file

@ -191,6 +191,15 @@ class ServerConfig:
# CRITICAL. # CRITICAL.
log_levels: dict[str, str] | None = None log_levels: dict[str, str] | None = None
# Flip this on to disable writing of Python bytecode (pyc) files. By
# default, pyc files are written to the cache directory under the
# game's config directory, and if you are iterating through lots of
# different config directories it is possible for this disk usage to
# add up. Not having pyc files available should have a pretty
# minimal impact on a server, unlike on a gui client where compiling
# modules on demand could cause visual hitches.
dont_write_bytecode: bool = False
# NOTE: as much as possible, communication from the server-manager to # NOTE: as much as possible, communication from the server-manager to
# the child-process should go through these and not ad-hoc Python string # the child-process should go through these and not ad-hoc Python string

View file

@ -37,18 +37,23 @@ class AssetsV1GlobalVals:
] = '' ] = ''
class AssetsV1StrInputTypeID(Enum): class AssetsV1StringFileTypeID(Enum):
"""Type ID for each of our subclasses.""" """Type ID for each of our subclasses."""
BASIC = 'b' V1 = 'v1'
class AssetsV1StrInput(IOMultiType[AssetsV1StrInputTypeID]): class AssetsV1StringFile(IOMultiType[AssetsV1StringFileTypeID]):
"""Top level class for our multitype.""" """Top level class for our multitype."""
@override @override
@classmethod @classmethod
def get_type_id(cls) -> AssetsV1StrInputTypeID: def get_type_id_storage_name(cls) -> str:
return 'string_file_version'
@override
@classmethod
def get_type_id(cls) -> AssetsV1StringFileTypeID:
# Require child classes to supply this themselves. If we did a # Require child classes to supply this themselves. If we did a
# full type registry/lookup here it would require us to import # full type registry/lookup here it would require us to import
# everything and would prevent lazy loading. # everything and would prevent lazy loading.
@ -57,14 +62,14 @@ class AssetsV1StrInput(IOMultiType[AssetsV1StrInputTypeID]):
@override @override
@classmethod @classmethod
def get_type( def get_type(
cls, type_id: AssetsV1StrInputTypeID cls, type_id: AssetsV1StringFileTypeID
) -> type[AssetsV1StrInput]: ) -> type[AssetsV1StringFile]:
"""Return the subclass for each of our type-ids.""" """Return the subclass for each of our type-ids."""
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
t = AssetsV1StrInputTypeID t = AssetsV1StringFileTypeID
if type_id is t.BASIC: if type_id is t.V1:
return BasicV1StrInput return AssetsV1StringFileV1
# Important to make sure we provide all types. # Important to make sure we provide all types.
assert_never(type_id) assert_never(type_id)
@ -72,42 +77,44 @@ class AssetsV1StrInput(IOMultiType[AssetsV1StrInputTypeID]):
@ioprepped @ioprepped
@dataclass @dataclass
class BasicV1StrInput(AssetsV1StrInput): class AssetsV1StringFileV1(AssetsV1StringFile):
"""Just a test.""" """Our initial version of string file data."""
class StylePreset(Enum):
"""Preset for general styling in translated strings."""
NONE = 'none'
TITLE = 'title'
INTENSE = 'intense'
SUBTLE = 'subtle'
@override @override
@classmethod @classmethod
def get_type_id(cls) -> AssetsV1StrInputTypeID: def get_type_id(cls) -> AssetsV1StringFileTypeID:
return AssetsV1StrInputTypeID.BASIC return AssetsV1StringFileTypeID.V1
@ioprepped
@dataclass
class AssetsV1StrData:
"""Data and output for a string asset."""
@dataclass @dataclass
class Output: class Output:
"""Represents a single instance of localized output.""" """Represents a single localized output."""
class Source(Enum): #: When this output was last changed.
"""Where localized output can come from.""" modtime: Annotated[
datetime.datetime, IOAttrs('modtime', float_times=True)
]
AI_V1 = 'ai_v1' #: Default value (no counts involved).
value: Annotated[str, IOAttrs('value')]
output_id: Annotated[int, IOAttrs('i')] input: Annotated[str, IOAttrs('input')]
created: Annotated[datetime.datetime, IOAttrs('c')] input_modtime: Annotated[
source: Annotated[Source, IOAttrs('s')] datetime.datetime, IOAttrs('input_modtime', float_times=True)
locale: Annotated[Locale, IOAttrs('l')] ]
value_default: Annotated[ style_preset: Annotated[
str | None, IOAttrs('d', store_default=False) StylePreset, IOAttrs('style_preset', store_default=False)
] = None ] = StylePreset.NONE
outputs: Annotated[dict[Locale, Output], IOAttrs('outputs')] = field(
inputs: Annotated[list[AssetsV1StrInput], IOAttrs('i')] = field( default_factory=dict
default_factory=list
) )
outputs: Annotated[list[Output], IOAttrs('o')] = field(default_factory=list)
next_output_id: Annotated[int, IOAttrs('n')] = 0
class AssetsV1PathValsTypeID(Enum): class AssetsV1PathValsTypeID(Enum):

View file

@ -19,6 +19,7 @@ from __future__ import annotations
import os import os
import sys import sys
import time import time
import random
import logging import logging
from pathlib import Path from pathlib import Path
from dataclasses import dataclass from dataclasses import dataclass
@ -26,10 +27,12 @@ from typing import TYPE_CHECKING
import __main__ import __main__
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any from typing import Any, Callable
from efro.logging import LogHandler from efro.logging import LogHandler
logger = logging.getLogger('ba.env')
# IMPORTANT - It is likely (and in some cases expected) that this # IMPORTANT - It is likely (and in some cases expected) that this
# module's code will be exec'ed multiple times. This is because it is # module's code will be exec'ed multiple times. This is because it is
# the job of this module to set up Python paths for an engine run, and # the job of this module to set up Python paths for an engine run, and
@ -38,7 +41,7 @@ if TYPE_CHECKING:
# /abs/path/to/ba_data/scripts/babase.py to ba_data/scripts/babase.py). # /abs/path/to/ba_data/scripts/babase.py to ba_data/scripts/babase.py).
# This can result in the next import of baenv loading us from our 'new' # This can result in the next import of baenv loading us from our 'new'
# location, which may or may not actually be the same file on disk as # location, which may or may not actually be the same file on disk as
# the last load. Either way, however, multiple execs will happen in some # the last load. Either way, however, multiple execs can happen in some
# form. # form.
# #
# To handle that situation gracefully, we need to do a few things: # To handle that situation gracefully, we need to do a few things:
@ -53,8 +56,8 @@ if TYPE_CHECKING:
# Build number and version of the ballistica binary we expect to be # Build number and version of the ballistica binary we expect to be
# using. # using.
TARGET_BALLISTICA_BUILD = 22381 TARGET_BALLISTICA_BUILD = 22535
TARGET_BALLISTICA_VERSION = '1.7.41' TARGET_BALLISTICA_VERSION = '1.7.51'
@dataclass @dataclass
@ -67,6 +70,10 @@ class EnvConfig:
#: Directory containing ba_data and any other platform-specific data. #: Directory containing ba_data and any other platform-specific data.
data_dir: str data_dir: str
#: Where cache files live (files generated by the app which can be
#: recreated if need be.
cache_dir: str
#: Where the app's built-in Python stuff lives. #: Where the app's built-in Python stuff lives.
app_python_dir: str | None app_python_dir: str | None
@ -76,20 +83,19 @@ class EnvConfig:
#: Where the app's bundled third party Python stuff lives. #: Where the app's bundled third party Python stuff lives.
site_python_dir: str | None site_python_dir: str | None
#: Custom Python provided by the user (mods). #: Where custom Python provided by the user (mods) lives.
user_python_dir: str | None user_python_dir: str | None
#: We have a mechanism allowing app scripts to be overridden by #: We have a mechanism allowing :attr:`app_python_dir` to be
#: placing a specially named directory in a user-scripts dir. This is #: overridden by placing a specially named directory in
#: true if that is enabled. #: :attr:`user_python_dir`. This is true if that is enabled.
is_user_app_python_dir: bool is_user_app_python_dir: bool
#: Our fancy app log handler. This handles feeding logs, stdout, and #: Our fancy app log handler. This handles feeding logs, stdout, and
#: stderr into the engine so they show up on in-app consoles, etc. #: stderr into the engine so they show up on in-app consoles, etc.
log_handler: LogHandler | None log_handler: LogHandler | None
#: Initial data from the config.json file in the config dir. The # Initial data from the ``config.json`` file in the config dir.
#: config file is parsed by
initial_app_config: Any initial_app_config: Any
#: Timestamp when we first started doing stuff. #: Timestamp when we first started doing stuff.
@ -122,17 +128,20 @@ class _EnvGlobals:
def did_paths_set_fail() -> bool: def did_paths_set_fail() -> bool:
"""Did we try to set paths and fail?""" """Did we try to set paths and fail?
:meta private:
"""
return _EnvGlobals.get().paths_set_failed return _EnvGlobals.get().paths_set_failed
def config_exists() -> bool: def env_config_exists() -> bool:
"""Has a config been created?""" """Has a config been created?"""
return _EnvGlobals.get().config is not None return _EnvGlobals.get().config is not None
def get_config() -> EnvConfig: def get_env_config() -> EnvConfig:
"""Return the active config, creating a default if none exists.""" """Return the active config, creating a default if none exists."""
envglobals = _EnvGlobals.get() envglobals = _EnvGlobals.get()
@ -143,7 +152,7 @@ def get_config() -> EnvConfig:
# paths to run Ballistica apps should be explicitly calling # paths to run Ballistica apps should be explicitly calling
# configure() first to get a full featured setup. # configure() first to get a full featured setup.
if not envglobals.called_configure: if not envglobals.called_configure:
configure(setup_logging=False) configure(setup_logging=False, setup_pycache_prefix=False)
config = envglobals.config config = envglobals.config
if config is None: if config is None:
@ -161,8 +170,11 @@ def configure(
user_python_dir: str | None = None, user_python_dir: str | None = None,
app_python_dir: str | None = None, app_python_dir: str | None = None,
site_python_dir: str | None = None, site_python_dir: str | None = None,
cache_dir: str | None = None,
contains_python_dist: bool = False, contains_python_dist: bool = False,
setup_logging: bool = True, setup_logging: bool = True,
setup_pycache_prefix: bool = False,
strict_threads_atexit: Callable[[Callable[[], None]], None] | None = None,
) -> None: ) -> None:
"""Set up the environment for running a Ballistica app. """Set up the environment for running a Ballistica app.
@ -170,6 +182,7 @@ def configure(
creation. This must be called before any actual Ballistica modules creation. This must be called before any actual Ballistica modules
are imported; the environment is locked in as soon as that happens. are imported; the environment is locked in as soon as that happens.
""" """
# pylint: disable=too-many-locals
# Measure when we start doing this stuff. We plug this in to show # Measure when we start doing this stuff. We plug this in to show
# relative times in our log timestamp displays and also pass this to # relative times in our log timestamp displays and also pass this to
@ -200,6 +213,7 @@ def configure(
site_python_dir, site_python_dir,
data_dir, data_dir,
config_dir, config_dir,
cache_dir,
standard_app_python_dir, standard_app_python_dir,
is_user_app_python_dir, is_user_app_python_dir,
) = _setup_paths( ) = _setup_paths(
@ -208,14 +222,27 @@ def configure(
site_python_dir, site_python_dir,
data_dir, data_dir,
config_dir, config_dir,
cache_dir,
) )
# The one other thing we do before setting up logging is redirect
# our pyc files to our cache dir. We want to do this is calced so
# that as much stuff as possible (efro.logging), etc.) will get its
# pyc files made in our custom cache dir.
prev_pycache_prefix = sys.pycache_prefix
if setup_pycache_prefix:
sys.pycache_prefix = os.path.join(cache_dir, 'pyc')
# Set up our log-handler and pipe Python's stdout/stderr into it. # Set up our log-handler and pipe Python's stdout/stderr into it.
# Later, once the engine comes up, the handler will feed its logs # Later, once the engine comes up, the handler will feed its logs
# (including cached history) to the os-specific output location. # (including cached history) to the os-specific output location.
# This means anything printed or logged at this point forward should # This means anything printed or logged at this point forward should
# be visible on all platforms. # be visible on all platforms.
log_handler = _create_log_handler(launch_time) if setup_logging else None log_handler = (
_create_log_handler(launch_time, strict_threads_atexit)
if setup_logging
else None
)
# Load the raw app-config dict. # Load the raw app-config dict.
app_config = _read_app_config(os.path.join(config_dir, 'config.json')) app_config = _read_app_config(os.path.join(config_dir, 'config.json'))
@ -226,13 +253,44 @@ def configure(
# We want to always be run in UTF-8 mode; complain if we're not. # We want to always be run in UTF-8 mode; complain if we're not.
if sys.flags.utf8_mode != 1: if sys.flags.utf8_mode != 1:
logging.warning( logger.warning(
"Python's UTF-8 mode is not set. Running Ballistica without" "Python's UTF-8 mode is not set. Running Ballistica without"
' it may lead to errors.' ' it may lead to errors.'
) )
# We (possibly) set pycache_prefix above so that opt .pyc files are
# written to the cache directory that we just set up, but ideally
# Python should have been set to that value at startup so that
# modules we've imported up to this point get cached there too.
#
# In most cases we can actually do this by calcing/setting the same
# path we use here before spinning up Python, but in some cases
# that's impossible (such as our _modular_main path below where we
# are already in Python before we get a chance to parse args that
# affect cache path).
#
# So let's warn here any time we're trying to set up pycache_prefix
# but find that we're setting it to a different value than it was
# already set to. We can inform the user (or ourselves) how to line
# things up using PYTHONPYCACHEPREFIX or whatnot.
if setup_pycache_prefix and prev_pycache_prefix != sys.pycache_prefix:
logger.warning(
'Changing sys.pycache_prefix from %s to %s.'
' For best performance, run with PYTHONPYCACHEPREFIX=%s.',
repr(prev_pycache_prefix),
repr(sys.pycache_prefix),
repr(sys.pycache_prefix),
)
# Attempt to create dirs that we'll write stuff to. # Attempt to create dirs that we'll write stuff to.
_setup_dirs(config_dir, user_python_dir) _setup_dirs(config_dir, user_python_dir, cache_dir)
# In debug builds, if we've not imported engine stuff yet, Kill off
# random cache files occasionally to help ensure that code responds
# correctly if/when the OS does the same thing.
if __debug__:
if '_babase' not in sys.modules:
_cache_ninja_rampage(cache_dir)
# Get ssl working if needed so we can use https and all that. # Get ssl working if needed so we can use https and all that.
_setup_certs(contains_python_dist) _setup_certs(contains_python_dist)
@ -241,6 +299,7 @@ def configure(
envglobals.config = EnvConfig( envglobals.config = EnvConfig(
config_dir=config_dir, config_dir=config_dir,
data_dir=data_dir, data_dir=data_dir,
cache_dir=cache_dir,
user_python_dir=user_python_dir, user_python_dir=user_python_dir,
app_python_dir=app_python_dir, app_python_dir=app_python_dir,
standard_app_python_dir=standard_app_python_dir, standard_app_python_dir=standard_app_python_dir,
@ -252,6 +311,21 @@ def configure(
) )
def _cache_ninja_rampage(cache_dir: str) -> None:
assert os.path.isdir(cache_dir)
for basename, _dirnames, filenames in os.walk(cache_dir):
for fname in filenames:
# Let's kill one out of every 1000 files; should be a
# reasonable amount of chaos I think. Can recalibrate this
# as our average cache file count goes up.
if random.random() < 0.001:
fullpath = os.path.join(basename, fname)
logging.getLogger('ba.cache').debug(
"Cache-ninja assasinated '%s'.", fullpath
)
os.unlink(fullpath)
def _read_app_config(config_file_path: str) -> dict: def _read_app_config(config_file_path: str) -> dict:
"""Read the app config.""" """Read the app config."""
import json import json
@ -269,7 +343,7 @@ def _read_app_config(config_file_path: str) -> dict:
config = {} config = {}
except Exception: except Exception:
logging.exception( logger.exception(
"Error reading config file '%s'.\n" "Error reading config file '%s'.\n"
"Backing up broken config to'%s.broken'.", "Backing up broken config to'%s.broken'.",
config_file_path, config_file_path,
@ -281,7 +355,7 @@ def _read_app_config(config_file_path: str) -> dict:
shutil.copyfile(config_file_path, config_file_path + '.broken') shutil.copyfile(config_file_path, config_file_path + '.broken')
except Exception: except Exception:
logging.exception('Error copying broken config.') logger.exception('Error copying broken config.')
config = {} config = {}
return config return config
@ -310,7 +384,10 @@ def _calc_data_dir(data_dir: str | None) -> str:
return data_dir return data_dir
def _create_log_handler(launch_time: float) -> LogHandler: def _create_log_handler(
launch_time: float,
strict_threads_atexit: Callable[[Callable[[], None]], None] | None,
) -> LogHandler:
from efro.logging import setup_logging, LogLevel from efro.logging import setup_logging, LogLevel
log_handler = setup_logging( log_handler = setup_logging(
@ -319,7 +396,18 @@ def _create_log_handler(launch_time: float) -> LogHandler:
log_stdout_stderr=True, log_stdout_stderr=True,
cache_size_limit=1024 * 1024, cache_size_limit=1024 * 1024,
launch_time=launch_time, launch_time=launch_time,
strict_threads=strict_threads_atexit is not None,
) )
# If we were given a strict_threads_atexit call, it means we should
# NOT use daemon threads but instead can use the atexit call to
# register a callback to gracefully exit our handler thread just
# before the interpreter shuts down. This is safer than using daemon
# threads, which can theoretically continue to use Python objs
# during and after interpreter shutdown.
if strict_threads_atexit is not None:
strict_threads_atexit(log_handler.shutdown)
return log_handler return log_handler
@ -359,7 +447,7 @@ def _set_log_levels(app_config: dict) -> None:
).apply() ).apply()
except Exception: except Exception:
logging.exception('Error setting log levels.') logger.exception('Error setting log levels.')
def _setup_certs(contains_python_dist: bool) -> None: def _setup_certs(contains_python_dist: bool) -> None:
@ -386,7 +474,10 @@ def _setup_paths(
site_python_dir: str | None, site_python_dir: str | None,
data_dir: str | None, data_dir: str | None,
config_dir: str | None, config_dir: str | None,
) -> tuple[str | None, str | None, str | None, str, str, str, bool]: cache_dir: str | None,
) -> tuple[str | None, str | None, str | None, str, str, str, str, bool]:
# pylint: disable=too-many-positional-arguments
# First a few paths we can ALWAYS calculate since they don't affect # First a few paths we can ALWAYS calculate since they don't affect
# Python imports: # Python imports:
@ -398,6 +489,10 @@ def _setup_paths(
if config_dir is None: if config_dir is None:
config_dir = str(Path(Path.home(), '.ballisticakit')) config_dir = str(Path(Path.home(), '.ballisticakit'))
# By default, cache-dir is simply 'cache' under config-dir.
if cache_dir is None:
cache_dir = str(Path(config_dir, 'cache'))
# Standard app-python-dir is simply ba_data/python under data-dir. # Standard app-python-dir is simply ba_data/python under data-dir.
standard_app_python_dir = str(Path(data_dir, 'ba_data', 'python')) standard_app_python_dir = str(Path(data_dir, 'ba_data', 'python'))
@ -422,7 +517,8 @@ def _setup_paths(
if app_python_dir is None: if app_python_dir is None:
app_python_dir = standard_app_python_dir app_python_dir = standard_app_python_dir
# Likewise site-python-dir defaults to ba_data/python-site-packages. # Likewise site-python-dir defaults to
# ba_data/python-site-packages.
if site_python_dir is None: if site_python_dir is None:
site_python_dir = str( site_python_dir = str(
Path(data_dir, 'ba_data', 'python-site-packages') Path(data_dir, 'ba_data', 'python-site-packages')
@ -447,7 +543,7 @@ def _setup_paths(
app_python_dir = str(check_dir) app_python_dir = str(check_dir)
is_user_app_python_dir = True is_user_app_python_dir = True
except PermissionError: except PermissionError:
logging.warning( logger.warning(
"PermissionError checking user-app-python-dir path '%s'.", "PermissionError checking user-app-python-dir path '%s'.",
check_dir, check_dir,
) )
@ -492,14 +588,18 @@ def _setup_paths(
site_python_dir, site_python_dir,
data_dir, data_dir,
config_dir, config_dir,
cache_dir,
standard_app_python_dir, standard_app_python_dir,
is_user_app_python_dir, is_user_app_python_dir,
) )
def _setup_dirs(config_dir: str | None, user_python_dir: str | None) -> None: def _setup_dirs(
config_dir: str | None, user_python_dir: str | None, cache_dir: str
) -> None:
create_dirs: list[tuple[str, str | None]] = [ create_dirs: list[tuple[str, str | None]] = [
('config', config_dir), ('config', config_dir),
('cache', cache_dir),
('user_python', user_python_dir), ('user_python', user_python_dir),
] ]
for cdirname, cdir in create_dirs: for cdirname, cdir in create_dirs:
@ -508,12 +608,12 @@ def _setup_dirs(config_dir: str | None, user_python_dir: str | None) -> None:
os.makedirs(cdir, exist_ok=True) os.makedirs(cdir, exist_ok=True)
except Exception: except Exception:
# Not the end of the world if we can't make these dirs. # Not the end of the world if we can't make these dirs.
logging.warning( logger.warning(
"Unable to create %s dir at '%s'.", cdirname, cdir "Unable to create %s dir at '%s'.", cdirname, cdir
) )
def extract_arg(args: list[str], names: list[str], is_dir: bool) -> str | None: def _extract_arg(args: list[str], names: list[str], is_dir: bool) -> str | None:
"""Given a list of args and an arg name, returns a value. """Given a list of args and an arg name, returns a value.
The arg flag and value are removed from the arg list. We also check The arg flag and value are removed from the arg list. We also check
@ -567,6 +667,7 @@ def _modular_main() -> None:
# command line. # command line.
try: try:
# Take note that we're running via modular-main. The native # Take note that we're running via modular-main. The native
# layer can key off this to know whether it should apply # layer can key off this to know whether it should apply
# sys.argv or not. # sys.argv or not.
@ -580,20 +681,21 @@ def _modular_main() -> None:
args = sys.argv.copy() args = sys.argv.copy()
# NOTE: We need to keep these arg long/short arg versions synced # NOTE: We need to keep these arg long/short arg versions synced
# to those in core_config.cc. That code parses these same args # to those in core_config.cc. That code will parse these same
# (even if it doesn't handle them in our case) and will complain # args (even if it doesn't do anything with them in this modular
# if unrecognized args come through. # path) and will complain if unrecognized args come through.
# Our -c arg basically mirrors Python's -c arg. If we get that, # Our -c arg basically mirrors Python's -c arg. If we get that,
# simply exec it and return; no engine stuff. # simply exec it and return; no engine stuff.
command = extract_arg(args, ['--command', '-c'], is_dir=False) command = _extract_arg(args, ['--command', '-c'], is_dir=False)
if command is not None: if command is not None:
exec(command) # pylint: disable=exec-used exec(command) # pylint: disable=exec-used
return return
config_dir = extract_arg(args, ['--config-dir', '-C'], is_dir=True) config_dir = _extract_arg(args, ['--config-dir', '-C'], is_dir=True)
data_dir = extract_arg(args, ['--data-dir', '-d'], is_dir=True) data_dir = _extract_arg(args, ['--data-dir', '-d'], is_dir=True)
mods_dir = extract_arg(args, ['--mods-dir', '-m'], is_dir=True) mods_dir = _extract_arg(args, ['--mods-dir', '-m'], is_dir=True)
cache_dir = _extract_arg(args, ['--cache-dir', '-a'], is_dir=True)
# We run configure() BEFORE importing babase. (part of its job # We run configure() BEFORE importing babase. (part of its job
# is to wrangle paths which can affect where babase and # is to wrangle paths which can affect where babase and
@ -602,6 +704,7 @@ def _modular_main() -> None:
config_dir=config_dir, config_dir=config_dir,
data_dir=data_dir, data_dir=data_dir,
user_python_dir=mods_dir, user_python_dir=mods_dir,
cache_dir=cache_dir,
) )
import babase import babase

View file

@ -9,17 +9,21 @@ want to compile the rest of the engine, or a fully open-source app can
also be built by removing this feature-set. also be built by removing this feature-set.
""" """
from __future__ import annotations # ba_meta require api 9
# Note: there's not much here. Most interaction with this feature-set # Note: Stuff in this module mostly exists for type-checking and docs
# should go through ba*.app.plus. # generation and should generally not be imported or used at runtime.
# Generally all interaction with this feature-set should go through
# `ba*.app.plus`.
import logging import logging
from baplus._cloud import CloudSubsystem from baplus._cloud import CloudSubsystem
from baplus._appsubsystem import PlusAppSubsystem from baplus._appsubsystem import PlusAppSubsystem
from baplus._ads import AdsSubsystem
__all__ = [ __all__ = [
'AdsSubsystem',
'CloudSubsystem', 'CloudSubsystem',
'PlusAppSubsystem', 'PlusAppSubsystem',
] ]

238
dist/ba_data/python/baplus/_ads.py vendored Normal file
View file

@ -0,0 +1,238 @@
# Released under the MIT License. See LICENSE for details.
#
"""Functionality related to ads."""
from __future__ import annotations
import time
import asyncio
import logging
from typing import TYPE_CHECKING
import babase
import _baplus
if TYPE_CHECKING:
from typing import Callable, Any
class AdsSubsystem:
"""Subsystem for ads functionality in the app.
Access the single shared instance of this class via the
:attr:`~baplus.PlusAppSubsystem.ads` attr on the
:class:`~baplus.PlusAppSubsystem` class.
"""
def __init__(self) -> None:
self.last_ad_network = 'unknown'
self.last_ad_network_set_time = time.time()
self.ad_amt: float | None = None
self.last_ad_purpose = 'invalid'
self.attempted_first_ad = False
self.last_in_game_ad_remove_message_show_time: float | None = None
self.last_ad_completion_time: float | None = None
self.last_ad_was_short = False
self._fallback_task: asyncio.Task | None = None
def do_remove_in_game_ads_message(self) -> None:
""":meta private:"""
# Print this message once every 10 minutes at most.
tval = babase.apptime()
if self.last_in_game_ad_remove_message_show_time is None or (
tval - self.last_in_game_ad_remove_message_show_time > 60 * 10
):
self.last_in_game_ad_remove_message_show_time = tval
with babase.ContextRef.empty():
babase.apptimer(
1.0,
lambda: babase.screenmessage(
babase.Lstr(
resource='removeInGameAdsTokenPurchaseText'
),
color=(1, 1, 0),
),
)
def can_show_ad(self) -> bool:
"""Can we show an ad?
:meta private:
"""
return _baplus.can_show_ad()
def has_video_ads(self) -> bool:
"""Are video ads available?
:meta private:
"""
return _baplus.has_video_ads()
def have_incentivized_ad(self) -> bool:
"""Is an incentivized ad available?
:meta private:
"""
return _baplus.have_incentivized_ad()
def show_ad(
self, purpose: str, on_completion_call: Callable[[], Any] | None = None
) -> None:
""":meta private:"""
self.last_ad_purpose = purpose
_baplus.show_ad(purpose, on_completion_call)
def show_ad_2(
self,
purpose: str,
on_completion_call: Callable[[bool], Any] | None = None,
) -> None:
""":meta private:"""
self.last_ad_purpose = purpose
_baplus.show_ad_2(purpose, on_completion_call)
def call_after_ad(self, call: Callable[[], Any]) -> None:
"""Run a call after potentially showing an ad."""
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
app = babase.app
plus = app.plus
classic = app.classic
assert plus is not None
assert classic is not None
show = True
# No ads without net-connections, etc.
if not self.can_show_ad():
show = False
if show:
interval: float | None
launch_count = app.config.get('launchCount', 0)
# If we're seeing short ads we may want to space them differently.
interval_mult = (
plus.get_v1_account_misc_read_val('ads.shortIntervalMult', 1.0)
if self.last_ad_was_short
else 1.0
)
if self.ad_amt is None:
if launch_count <= 1:
self.ad_amt = plus.get_v1_account_misc_read_val(
'ads.startVal1', 0.99
)
else:
self.ad_amt = plus.get_v1_account_misc_read_val(
'ads.startVal2', 1.0
)
interval = None
else:
# So far we're cleared to show; now calc our
# ad-show-threshold and see if we should *actually* show
# (we reach our threshold faster the longer we've been
# playing).
base = 'ads' if self.has_video_ads() else 'ads2'
min_lc = plus.get_v1_account_misc_read_val(base + '.minLC', 0.0)
max_lc = plus.get_v1_account_misc_read_val(base + '.maxLC', 5.0)
min_lc_scale = plus.get_v1_account_misc_read_val(
base + '.minLCScale', 0.25
)
max_lc_scale = plus.get_v1_account_misc_read_val(
base + '.maxLCScale', 0.34
)
min_lc_interval = plus.get_v1_account_misc_read_val(
base + '.minLCInterval', 360
)
max_lc_interval = plus.get_v1_account_misc_read_val(
base + '.maxLCInterval', 300
)
if launch_count < min_lc:
lc_amt = 0.0
elif launch_count > max_lc:
lc_amt = 1.0
else:
lc_amt = (float(launch_count) - min_lc) / (max_lc - min_lc)
incr = (1.0 - lc_amt) * min_lc_scale + lc_amt * max_lc_scale
interval = (
1.0 - lc_amt
) * min_lc_interval + lc_amt * max_lc_interval
self.ad_amt += incr
assert self.ad_amt is not None
if self.ad_amt >= 1.0:
self.ad_amt = self.ad_amt % 1.0
self.attempted_first_ad = True
# After we've reached the traditional show-threshold once,
# try again whenever its been INTERVAL since our last successful
# show.
elif self.attempted_first_ad and (
self.last_ad_completion_time is None
or (
interval is not None
and babase.apptime() - self.last_ad_completion_time
> (interval * interval_mult)
)
):
# Reset our other counter too in this case.
self.ad_amt = 0.0
else:
show = False
# If we're *still* cleared to show, tell the system to show.
if show:
# As a safety-check, we set up an object that will run the
# completion callback if we've returned and sat for several
# seconds (in case some random ad network doesn't properly
# deliver its completion callback).
payload = _AdPayload(call)
# Set up our backup.
with babase.ContextRef.empty():
# Note to self: Previously this was a simple 5 second
# timer because the app got totally suspended while ads
# were showing (which delayed the timer), but these days
# the app may continue to run, so we need to be more
# careful and only fire the fallback after we see that
# the app has been front-and-center for several seconds.
async def add_fallback_task() -> None:
activesecs = 5
while activesecs > 0:
if babase.app.active:
activesecs -= 1
await asyncio.sleep(1.0)
payload.run(fallback=True)
babase.app.create_async_task(add_fallback_task())
self.show_ad('between_game', on_completion_call=payload.run)
else:
babase.pushcall(call) # Just run the callback without the ad.
class _AdPayload:
def __init__(self, pcall: Callable[[], Any]):
self._call = pcall
self._ran = False
def run(self, fallback: bool = False) -> None:
"""Run the payload."""
plus = babase.app.plus
assert plus is not None
if not self._ran:
if fallback:
lanst = plus.ads.last_ad_network_set_time
logging.error(
'Relying on fallback ad-callback! '
'last network: %s (set %s seconds ago);'
' purpose=%s.',
plus.ads.last_ad_network,
time.time() - lanst,
plus.ads.last_ad_purpose,
)
babase.pushcall(self._call)
self._ran = True

View file

@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, override
from babase import AppSubsystem from babase import AppSubsystem
import _baplus import _baplus
from baplus._ads import AdsSubsystem
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Callable, Any from typing import Callable, Any
@ -29,13 +30,14 @@ class PlusAppSubsystem(AppSubsystem):
# pylint: disable=too-many-public-methods # pylint: disable=too-many-public-methods
# Note: this is basically just a wrapper around _baplus for
# type-checking purposes. Maybe there's some smart way we could skip
# the overhead of this wrapper at runtime.
accounts: AccountV2Subsystem accounts: AccountV2Subsystem
cloud: CloudSubsystem cloud: CloudSubsystem
def __init__(self) -> None:
#: Ad wrangling functionality.
self.ads: AdsSubsystem = AdsSubsystem()
@override @override
def on_app_loading(self) -> None: def on_app_loading(self) -> None:
""":meta private:""" """:meta private:"""
@ -59,12 +61,36 @@ class PlusAppSubsystem(AppSubsystem):
return _baplus.game_service_has_leaderboard(game, config) return _baplus.game_service_has_leaderboard(game, config)
@staticmethod @staticmethod
def get_master_server_address(source: int = -1, version: int = 1) -> str: def get_legacy_master_server_address() -> str:
"""Return the address of the old master server.
:meta private:
"""
return _baplus.get_legacy_master_server_address()
@staticmethod
def get_master_server_address() -> str:
"""Return the address of the master server. """Return the address of the master server.
:meta private: :meta private:
""" """
return _baplus.get_master_server_address(source, version) return _baplus.get_master_server_address()
@staticmethod
def get_bootstrap_server_addresses() -> list[str]:
"""Return addresses we can use to establish regional connection.
:meta private:
"""
return _baplus.get_bootstrap_server_addresses()
@staticmethod
def get_bootstrap_server_address() -> str:
"""Return address we can use to establish regional connection.
:meta private:
"""
return _baplus.get_bootstrap_server_address()
@staticmethod @staticmethod
def get_classic_news_show() -> str: def get_classic_news_show() -> str:
@ -76,16 +102,6 @@ class PlusAppSubsystem(AppSubsystem):
""":meta private:""" """:meta private:"""
return _baplus.get_price(item) return _baplus.get_price(item)
@staticmethod
def get_v1_account_product_purchased(item: str) -> bool:
""":meta private:"""
return _baplus.get_v1_account_product_purchased(item)
@staticmethod
def get_v1_account_product_purchases_state() -> int:
""":meta private:"""
return _baplus.get_v1_account_product_purchases_state()
@staticmethod @staticmethod
def get_v1_account_display_string(full: bool = True) -> str: def get_v1_account_display_string(full: bool = True) -> str:
""":meta private:""" """:meta private:"""
@ -126,13 +142,13 @@ class PlusAppSubsystem(AppSubsystem):
""":meta private:""" """:meta private:"""
return _baplus.get_v1_account_state_num() return _baplus.get_v1_account_state_num()
@staticmethod # @staticmethod
def get_v1_account_ticket_count() -> int: # def get_v1_account_ticket_count() -> int:
"""Return the number of tickets for the current account. # """Return the number of tickets for the current account.
:meta private: # :meta private:
""" # """
return _baplus.get_v1_account_ticket_count() # return _baplus.get_v1_account_ticket_count()
@staticmethod @staticmethod
def get_v1_account_type() -> str: def get_v1_account_type() -> str:
@ -257,50 +273,6 @@ class PlusAppSubsystem(AppSubsystem):
""" """
return _baplus.supports_purchases() return _baplus.supports_purchases()
@staticmethod
def have_incentivized_ad() -> bool:
"""Is an incentivized ad available?
:meta private:
"""
return _baplus.have_incentivized_ad()
@staticmethod
def has_video_ads() -> bool:
"""Are video ads available?
:meta private:
"""
return _baplus.has_video_ads()
@staticmethod
def can_show_ad() -> bool:
"""Can we show an ad?
:meta private:
"""
return _baplus.can_show_ad()
@staticmethod
def show_ad(
purpose: str, on_completion_call: Callable[[], None] | None = None
) -> None:
"""Show an ad.
:meta private:
"""
_baplus.show_ad(purpose, on_completion_call)
@staticmethod
def show_ad_2(
purpose: str, on_completion_call: Callable[[bool], None] | None = None
) -> None:
"""Show an ad.
:meta private:
"""
_baplus.show_ad_2(purpose, on_completion_call)
@staticmethod @staticmethod
def show_game_service_ui( def show_game_service_ui(
show: str = 'general', show: str = 'general',

View file

@ -4,17 +4,21 @@
from __future__ import annotations from __future__ import annotations
import time
import logging import logging
from typing import TYPE_CHECKING, overload from typing import TYPE_CHECKING, overload
from efro.error import CommunicationError
from efro.call import CallbackSet from efro.call import CallbackSet
from efro.dataclassio import dataclass_from_dict, dataclass_to_dict
import bacommon.bs
import bacommon.cloud
import babase import babase
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Callable, Any from typing import Callable, Any
from efro.message import Message, Response from efro.message import Message, Response, BoolResponse
import bacommon.cloud
import bacommon.bs import bacommon.bs
@ -31,12 +35,41 @@ class CloudSubsystem(babase.AppSubsystem):
:class:`~baplus.PlusAppSubsystem` class. :class:`~baplus.PlusAppSubsystem` class.
""" """
#: General engine config values provided by the cloud.
vals: bacommon.cloud.CloudVals
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
self.on_connectivity_changed_callbacks: CallbackSet[ self.on_connectivity_changed_callbacks: CallbackSet[
Callable[[bool], None] Callable[[bool], None]
] = CallbackSet() ] = CallbackSet()
# Restore saved cloud-vals (or init to default).
try:
cloudvals_data = babase.app.config.get('CloudVals')
if isinstance(cloudvals_data, dict):
self.vals = dataclass_from_dict(
bacommon.cloud.CloudVals, cloudvals_data
)
else:
self.vals = bacommon.cloud.CloudVals()
except Exception:
babase.applog.warning(
'Error loading CloudVals; resetting to default.', exc_info=True
)
self.vals = bacommon.cloud.CloudVals()
# Set up to start updating cloud-vals once we've got
# connectivity.
self._vals_updated = False
self._vals_update_timer: babase.AppTimer | None = None
self._vals_last_request_time: float | None = None
self._vals_update_conn_reg = (
self.on_connectivity_changed_callbacks.register(
self._update_vals_update_for_connectivity
)
)
@property @property
def connected(self) -> bool: def connected(self) -> bool:
"""Whether a connection to the cloud is present. """Whether a connection to the cloud is present.
@ -70,6 +103,55 @@ class CloudSubsystem(babase.AppSubsystem):
except Exception: except Exception:
logging.exception('Error in connectivity-changed callback.') logging.exception('Error in connectivity-changed callback.')
def _update_vals_update_for_connectivity(self, connected: bool) -> None:
# If we don't have vals yet and are connected, start asking.
if connected and not self._vals_updated:
# Ask immediately and set up a timer to keep doing so until
# successful.
self._possibly_send_vals_request()
self._vals_update_timer = babase.AppTimer(
61.23, self._possibly_send_vals_request, repeat=True
)
else:
# Ok; we're disconnected or have vals - stop asking.
self._vals_update_timer = None
def _possibly_send_vals_request(self) -> None:
now = time.monotonic()
# Only send if we havn't already recently.
if (
self._vals_last_request_time is None
or now - self._vals_last_request_time > 30.0
):
self._vals_last_request_time = now
self.send_message_cb(
bacommon.cloud.CloudValsRequest(), self._on_cloud_vals_response
)
def _on_cloud_vals_response(
self, response: bacommon.cloud.CloudValsResponse | Exception
) -> None:
if isinstance(response, Exception):
# Make noise for any non-communication errors
if not isinstance(response, CommunicationError):
babase.applog.exception(
'Unexpected error in _on_cloud_vals_response().'
)
return
# If what we got differs from what we already had, store it.
if response.vals != self.vals:
cfg = babase.app.config
cfg['CloudVals'] = dataclass_to_dict(response.vals)
cfg.commit()
self.vals = response.vals
# We can stop asking now.
self._vals_updated = True
self._vals_update_timer = None
@overload @overload
def send_message_cb( def send_message_cb(
self, self,
@ -79,6 +161,15 @@ class CloudSubsystem(babase.AppSubsystem):
], ],
) -> None: ... ) -> None: ...
@overload
def send_message_cb(
self,
msg: bacommon.cloud.CloudValsRequest,
on_response: Callable[
[bacommon.cloud.CloudValsResponse | Exception], None
],
) -> None: ...
@overload @overload
def send_message_cb( def send_message_cb(
self, self,
@ -120,6 +211,15 @@ class CloudSubsystem(babase.AppSubsystem):
], ],
) -> None: ... ) -> None: ...
@overload
def send_message_cb(
self,
msg: bacommon.bs.GetClassicPurchasesMessage,
on_response: Callable[
[bacommon.bs.GetClassicPurchasesResponse | Exception], None
],
) -> None: ...
@overload @overload
def send_message_cb( def send_message_cb(
self, self,
@ -174,6 +274,13 @@ class CloudSubsystem(babase.AppSubsystem):
], ],
) -> None: ... ) -> None: ...
@overload
def send_message_cb(
self,
msg: bacommon.bs.GlobalProfileCheckMessage,
on_response: Callable[[BoolResponse | Exception], None],
) -> None: ...
@overload @overload
def send_message_cb( def send_message_cb(
self, self,
@ -230,6 +337,11 @@ class CloudSubsystem(babase.AppSubsystem):
self, msg: bacommon.cloud.TestMessage self, msg: bacommon.cloud.TestMessage
) -> bacommon.cloud.TestResponse: ... ) -> bacommon.cloud.TestResponse: ...
@overload
def send_message(
self, msg: bacommon.bs.LegacyRequest
) -> bacommon.bs.LegacyResponse: ...
def send_message(self, msg: Message) -> Response | None: def send_message(self, msg: Message) -> Response | None:
"""Synchronously send a message to the cloud. """Synchronously send a message to the cloud.
@ -241,8 +353,8 @@ class CloudSubsystem(babase.AppSubsystem):
@overload @overload
async def send_message_async( async def send_message_async(
self, msg: bacommon.cloud.SendInfoMessage self, msg: bacommon.bs.SendInfoMessage
) -> bacommon.cloud.SendInfoResponse: ... ) -> bacommon.bs.SendInfoResponse: ...
@overload @overload
async def send_message_async( async def send_message_async(
@ -321,6 +433,7 @@ def cloud_console_exec(code: str) -> None:
execcode = compile(code, '<console>', 'exec') execcode = compile(code, '<console>', 'exec')
# pylint: disable=exec-used # pylint: disable=exec-used
exec(execcode, vars(__main__), vars(__main__)) exec(execcode, vars(__main__), vars(__main__))
except Exception: except Exception:
import traceback import traceback

View file

@ -71,7 +71,7 @@ from _bascenev1 import (
basetimer, basetimer,
BaseTimer, BaseTimer,
camerashake, camerashake,
capture_gamepad_input, capture_game_controller_input,
capture_keyboard_input, capture_keyboard_input,
chatmessage, chatmessage,
client_info_query_response, client_info_query_response,
@ -94,7 +94,7 @@ from _bascenev1 import (
get_public_party_max_size, get_public_party_max_size,
get_random_names, get_random_names,
get_replay_speed_exponent, get_replay_speed_exponent,
get_ui_input_device, get_main_ui_input_device,
getactivity, getactivity,
getcollisionmesh, getcollisionmesh,
getdata, getdata,
@ -122,8 +122,9 @@ from _bascenev1 import (
pause_replay, pause_replay,
printnodes, printnodes,
protocol_version, protocol_version,
release_gamepad_input, release_game_controller_input,
release_keyboard_input, release_keyboard_input,
reload_hooks,
reset_random_player_names, reset_random_player_names,
resume_replay, resume_replay,
seek_replay, seek_replay,
@ -277,7 +278,7 @@ __all__ = [
'cameraflash', 'cameraflash',
'camerashake', 'camerashake',
'Campaign', 'Campaign',
'capture_gamepad_input', 'capture_game_controller_input',
'capture_keyboard_input', 'capture_keyboard_input',
'CelebrateMessage', 'CelebrateMessage',
'chatmessage', 'chatmessage',
@ -346,7 +347,7 @@ __all__ = [
'get_remote_app_name', 'get_remote_app_name',
'get_replay_speed_exponent', 'get_replay_speed_exponent',
'get_trophy_string', 'get_trophy_string',
'get_ui_input_device', 'get_main_ui_input_device',
'getactivity', 'getactivity',
'getcollision', 'getcollision',
'getcollisionmesh', 'getcollisionmesh',
@ -413,8 +414,9 @@ __all__ = [
'protocol_version', 'protocol_version',
'pushcall', 'pushcall',
'register_map', 'register_map',
'release_gamepad_input', 'release_game_controller_input',
'release_keyboard_input', 'release_keyboard_input',
'reload_hooks',
'reset_random_player_names', 'reset_random_player_names',
'resume_replay', 'resume_replay',
'seek_replay', 'seek_replay',

View file

@ -43,16 +43,22 @@ class EndSessionActivity(Activity[EmptyPlayer, EmptyTeam]):
def on_begin(self) -> None: def on_begin(self) -> None:
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
assert babase.app.classic is not None classic = babase.app.classic
plus = babase.app.plus
assert classic is not None
assert plus is not None
main_menu_session = babase.app.classic.get_main_menu_session() main_menu_session = classic.get_main_menu_session()
super().on_begin() super().on_begin()
babase.unlock_all_input() babase.unlock_all_input()
assert babase.app.classic is not None assert babase.app.plus is not None
babase.app.classic.ads.call_after_ad(
babase.Call(_bascenev1.new_host_session, main_menu_session) call = babase.Call(_bascenev1.new_host_session, main_menu_session)
) if classic.can_show_interstitial():
plus.ads.call_after_ad(call)
else:
babase.pushcall(call)
class JoinActivity(Activity[EmptyPlayer, EmptyTeam]): class JoinActivity(Activity[EmptyPlayer, EmptyTeam]):

View file

@ -57,8 +57,12 @@ class CoopGameActivity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team](
super().on_begin() super().on_begin()
# Show achievements remaining. # Show achievements remaining.
env = babase.app.env
if not (env.demo or env.arcade): variant = babase.app.env.variant
vart = type(variant)
arcade_or_demo = variant is vart.ARCADE or variant is vart.DEMO
if not arcade_or_demo:
_bascenev1.timer( _bascenev1.timer(
3.8, babase.WeakCall(self._show_remaining_achievements) 3.8, babase.WeakCall(self._show_remaining_achievements)
) )

View file

@ -318,13 +318,17 @@ class CoopSession(Session):
else: else:
next_game = self._current_game_instance next_game = self._current_game_instance
variant = babase.app.env.variant
vart = type(variant)
arcade_or_demo = variant is vart.ARCADE or variant is vart.DEMO
# Special case: if we're coming from a joining-activity # Special case: if we're coming from a joining-activity
# and will be going into onslaught-training, show the # and will be going into onslaught-training, show the
# tutorial first. # tutorial first.
if ( if (
isinstance(activity, JoinActivity) isinstance(activity, JoinActivity)
and self.campaign_level_name == 'Onslaught Training' and self.campaign_level_name == 'Onslaught Training'
and not (env.demo or env.arcade) and not arcade_or_demo
): ):
if self._tutorial_activity is None: if self._tutorial_activity is None:
raise RuntimeError('Tutorial not preloaded properly.') raise RuntimeError('Tutorial not preloaded properly.')
@ -346,7 +350,7 @@ class CoopSession(Session):
# Now flip the current activity.. # Now flip the current activity..
self.setactivity(next_game) self.setactivity(next_game)
if not (env.demo or env.arcade): if not arcade_or_demo:
if ( if (
self.tournament_id is not None self.tournament_id is not None
and classic.coop_session_args['submit_score'] and classic.coop_session_args['submit_score']

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