mirror of
https://github.com/imayushsaini/Bombsquad-Ballistica-Modded-Server.git
synced 2025-10-20 00:00:39 +00:00
syncing ballistica 1.7.50
This commit is contained in:
parent
dd4dfed507
commit
3047591b10
187 changed files with 9472 additions and 4302 deletions
53
dist/ba_data/data/langdata.json
vendored
53
dist/ba_data/data/langdata.json
vendored
|
|
@ -20,6 +20,7 @@
|
|||
"Indonesian": "Bahasa Indonesia",
|
||||
"Italian": "Italiano",
|
||||
"Japanese": "日本語",
|
||||
"Kazakh": "Қазақша",
|
||||
"Korean": "한국어",
|
||||
"Malay": "Melayu",
|
||||
"Persian": "فارسی",
|
||||
|
|
@ -102,6 +103,7 @@
|
|||
"Gifasa abidjahsi",
|
||||
"Abinav",
|
||||
"Abir",
|
||||
"Abishek",
|
||||
"ABITDANTON",
|
||||
"Abne",
|
||||
"Abolfadl",
|
||||
|
|
@ -114,6 +116,7 @@
|
|||
"adan",
|
||||
"Adeel (AdeZ {@adez_})",
|
||||
"Adel",
|
||||
"adeline",
|
||||
"AdemYzz",
|
||||
"Rio adi",
|
||||
"Rayhan Adiansyah",
|
||||
|
|
@ -284,6 +287,7 @@
|
|||
"Asshold",
|
||||
"Eliane Santos de assis",
|
||||
"Atalanta",
|
||||
"Atayk00",
|
||||
"Atilla",
|
||||
"Atom",
|
||||
"Attila",
|
||||
|
|
@ -311,6 +315,7 @@
|
|||
"Myth B.",
|
||||
"B4likeBefore",
|
||||
"Praveen Babu",
|
||||
"Baby🇭🇹",
|
||||
"Badmoss",
|
||||
"Baechu",
|
||||
"bag",
|
||||
|
|
@ -327,6 +332,7 @@
|
|||
"Ibrahim Baraka",
|
||||
"Kamil Barański (Limak09)",
|
||||
"Leonan Barcelos",
|
||||
"Bardak56",
|
||||
"Bardiaghasedipour",
|
||||
"William Barnak",
|
||||
"William Barnakk",
|
||||
|
|
@ -372,6 +378,7 @@
|
|||
"Anton Bang Berner",
|
||||
"Felix Bernhard",
|
||||
"Beroudzin",
|
||||
"Besho",
|
||||
"Abhishek Bhardwaj",
|
||||
"Bhxyu",
|
||||
"Arthur Bianco",
|
||||
|
|
@ -403,6 +410,7 @@
|
|||
"Gianfranco Del Borrello",
|
||||
"Abel Borso",
|
||||
"Plasma Boson",
|
||||
"Cristi bossul",
|
||||
"Cristian Bote \"ZZAZZ\"",
|
||||
"Cristián Bote",
|
||||
"botris",
|
||||
|
|
@ -473,6 +481,7 @@
|
|||
"Ceren",
|
||||
"cflagos",
|
||||
"cgaming",
|
||||
"Chammmbeo",
|
||||
"chang",
|
||||
"Charlie",
|
||||
"Pradip Chaudhari",
|
||||
|
|
@ -575,6 +584,8 @@
|
|||
"df",
|
||||
"DFгульСпачибо",
|
||||
"Santanu Dhar",
|
||||
"Dheeraj",
|
||||
"Dhei8dje3",
|
||||
"DHRUVIL",
|
||||
"DIAbli",
|
||||
"Diablo",
|
||||
|
|
@ -640,6 +651,7 @@
|
|||
"EgorZH",
|
||||
"Ali ehs",
|
||||
"eightyfoahh",
|
||||
"EiOi",
|
||||
"Eiva",
|
||||
"EK",
|
||||
"EKFH",
|
||||
|
|
@ -652,6 +664,7 @@
|
|||
"ElDemon",
|
||||
"ElderLink",
|
||||
"elfree",
|
||||
"Peter Elia",
|
||||
"Elian",
|
||||
"Elmakyt",
|
||||
"ELMEX95",
|
||||
|
|
@ -680,6 +693,7 @@
|
|||
"Bueno pero igual era",
|
||||
"Era0S (Spazton)",
|
||||
"EraOS",
|
||||
"Erenbabapro",
|
||||
"Erfan",
|
||||
"Eric-fan",
|
||||
"Erick",
|
||||
|
|
@ -699,6 +713,7 @@
|
|||
"EXTENDOO",
|
||||
"Eyder",
|
||||
"Eymen",
|
||||
"Leen Ezrin",
|
||||
"F15fahd_lol",
|
||||
"fa9oly9",
|
||||
"Fabian",
|
||||
|
|
@ -855,6 +870,7 @@
|
|||
"Hack",
|
||||
"HackPlayer697",
|
||||
"hadi",
|
||||
"Hafod",
|
||||
"hafzanpajan",
|
||||
"Haidar",
|
||||
"Joud haidar",
|
||||
|
|
@ -864,6 +880,7 @@
|
|||
"HamCam1015",
|
||||
"hamed",
|
||||
"Alhasan Hamoud/Alretrox ❤️🔥🖤",
|
||||
"HamzaDriss",
|
||||
"Zulfikar Hanif",
|
||||
"Happaphus",
|
||||
"Franschieko Satya Haprabu",
|
||||
|
|
@ -874,6 +891,7 @@
|
|||
"Hasan",
|
||||
"Mohammad hasan",
|
||||
"Hisham bin Hashim",
|
||||
"Hashtag",
|
||||
"Emil Hauge",
|
||||
"Arian Haxhijaj",
|
||||
"Ergin Haxhijaj",
|
||||
|
|
@ -934,6 +952,7 @@
|
|||
"Hussam",
|
||||
"Huy",
|
||||
"HyMr",
|
||||
"Popsie (formely HyMr)",
|
||||
"HYr",
|
||||
"Adrian Höfer",
|
||||
"Davide Iaccarino",
|
||||
|
|
@ -967,6 +986,7 @@
|
|||
"indieGEARgames",
|
||||
"Darkness indo",
|
||||
"Indohuman",
|
||||
"Infray",
|
||||
"Inicio",
|
||||
"IniSaya6666",
|
||||
"inkMedic",
|
||||
|
|
@ -1040,7 +1060,9 @@
|
|||
"Steven john",
|
||||
"Johnny",
|
||||
"Johnwick",
|
||||
"johnyzzh",
|
||||
"joke",
|
||||
"JolteonLover2050Electric",
|
||||
"jonas-bonas",
|
||||
"Jonatas",
|
||||
"Jonathan",
|
||||
|
|
@ -1089,12 +1111,15 @@
|
|||
"Karim",
|
||||
"Karlimero",
|
||||
"shemas - Ebrahim Karram",
|
||||
"karry",
|
||||
"Kasra",
|
||||
"katon",
|
||||
"Kau5hik",
|
||||
"Alex Kaufman",
|
||||
"Kaunyt",
|
||||
"Kaushik",
|
||||
"KawaiiON",
|
||||
"KAZDOG",
|
||||
"kazoo081",
|
||||
"kazooicek",
|
||||
"KD",
|
||||
|
|
@ -1191,6 +1216,7 @@
|
|||
"Lemon4ik",
|
||||
"Leo",
|
||||
"Mr. LeoLeo",
|
||||
"Leonar",
|
||||
"Leonid",
|
||||
"Lepixhd",
|
||||
"Lester",
|
||||
|
|
@ -1207,6 +1233,7 @@
|
|||
"Nicola Ligas",
|
||||
"Alef costa lima",
|
||||
"Limak09",
|
||||
"Limber",
|
||||
"LimonAga",
|
||||
"lin",
|
||||
"Dustin Lin",
|
||||
|
|
@ -1362,6 +1389,7 @@
|
|||
"Medic药",
|
||||
"German Medin",
|
||||
"Martin Medina",
|
||||
"Mega",
|
||||
"Mehret Mehanzel",
|
||||
"Mobin Mehdizadeh",
|
||||
"Mehmet",
|
||||
|
|
@ -1416,6 +1444,7 @@
|
|||
"Mohammad11dembele",
|
||||
"MOHAMMADERFAN",
|
||||
"Mohammadhosain",
|
||||
"MohammadMardi",
|
||||
"Mohammadpl",
|
||||
"Mohammed",
|
||||
"MohammedTalal1st",
|
||||
|
|
@ -1447,6 +1476,7 @@
|
|||
"MrDaniel715",
|
||||
"MrGlu10free",
|
||||
"Mrmaxmeier",
|
||||
"MrMeme",
|
||||
"MrNexis",
|
||||
"MrS0meone",
|
||||
"MrSaster2024",
|
||||
|
|
@ -1534,6 +1564,7 @@
|
|||
"NoNameA14171274.",
|
||||
"NoNameC3698241",
|
||||
"None",
|
||||
"Nonspe 1120",
|
||||
"NOOBPEDAR",
|
||||
"NoobPilotPlayz",
|
||||
"Noobslaya101",
|
||||
|
|
@ -1562,6 +1593,7 @@
|
|||
"On3GaMs",
|
||||
"No one",
|
||||
"oneman",
|
||||
"Ornstein",
|
||||
"Adam Oros",
|
||||
"Andrés Ortega",
|
||||
"Zangar Orynbetov",
|
||||
|
|
@ -1655,6 +1687,7 @@
|
|||
"Pong",
|
||||
"Lehlogonolo \"YetNT\" Poole",
|
||||
"Pooya",
|
||||
"Popsie",
|
||||
"pouriya",
|
||||
"Pouya",
|
||||
"pranav",
|
||||
|
|
@ -1762,12 +1795,14 @@
|
|||
"Rishabh",
|
||||
"Rivki",
|
||||
"rizaldy",
|
||||
"Robert",
|
||||
"Rodbert",
|
||||
"Rodrigo",
|
||||
"Giovanni Rodríguez",
|
||||
"Marco Rodríguez",
|
||||
"Rohan",
|
||||
"Rohit",
|
||||
"rojman",
|
||||
"Bihary Roland",
|
||||
"Jericho roldan",
|
||||
"Roma :D",
|
||||
|
|
@ -1790,8 +1825,10 @@
|
|||
"RussianLover2008",
|
||||
"Ryan",
|
||||
"LiÇViN:Cviatkoú Kanstançin Rygoravič",
|
||||
"Šimon S.",
|
||||
"Ricky Joe S.Flores",
|
||||
"Rami Sabbagh",
|
||||
"Sadramj91",
|
||||
"Justin Saephan",
|
||||
"sahel",
|
||||
"Abdullah Saim",
|
||||
|
|
@ -1806,6 +1843,7 @@
|
|||
"Salted",
|
||||
"Matteo Salvini",
|
||||
"Salvo04",
|
||||
"Sam",
|
||||
"SamComeli",
|
||||
"Ali Sameer",
|
||||
"Samen",
|
||||
|
|
@ -1820,6 +1858,7 @@
|
|||
"Guilherme Santana",
|
||||
"Santiago",
|
||||
"Ivan Santos :)",
|
||||
"Nicolas Vicente dos Santos",
|
||||
"santosamerica880@gmail.com",
|
||||
"Diamond Sanwich",
|
||||
"SAO_OMH",
|
||||
|
|
@ -1883,6 +1922,7 @@
|
|||
"Slavik❤",
|
||||
"SlayTaniK",
|
||||
"slcat",
|
||||
"Slmnd",
|
||||
"Igor Slobodchuk",
|
||||
"Rasim Smaili",
|
||||
"Nicola Smaniotto",
|
||||
|
|
@ -1894,6 +1934,9 @@
|
|||
"Matheus Soares",
|
||||
"sobhan",
|
||||
"Nikhil sohan",
|
||||
"Soheib",
|
||||
"SoheibAk",
|
||||
"soheil",
|
||||
"SoK",
|
||||
"SoldierBS",
|
||||
"Unnamed Solicitude",
|
||||
|
|
@ -2006,6 +2049,7 @@
|
|||
"Cristian Ticu",
|
||||
"Robert Tieber",
|
||||
"TieDan",
|
||||
"250 Tier",
|
||||
"Tigas",
|
||||
"TIGEE",
|
||||
"Tim",
|
||||
|
|
@ -2015,6 +2059,7 @@
|
|||
"tjkffndeupwfbkh",
|
||||
"Juraj Tlach",
|
||||
"TM-DoDo",
|
||||
"Tocinito",
|
||||
"Toloche",
|
||||
"Tom",
|
||||
"Juan Pablo Montoya Tomalá",
|
||||
|
|
@ -2060,6 +2105,7 @@
|
|||
"Usishshsis",
|
||||
"utyrrwq",
|
||||
"Uzinerz",
|
||||
"Uzma",
|
||||
"Uładzisłaŭ",
|
||||
"Shohrux V",
|
||||
"Vader",
|
||||
|
|
@ -2085,6 +2131,7 @@
|
|||
"vijay",
|
||||
"vinicius",
|
||||
"Robin Vinith",
|
||||
"Casper Vinkle",
|
||||
"vinoth",
|
||||
"Vishal",
|
||||
"VISHUUU",
|
||||
|
|
@ -2109,7 +2156,9 @@
|
|||
"Wakefield",
|
||||
"Simon Wang",
|
||||
"Will Wang",
|
||||
"Watermelon",
|
||||
"WeanCZ",
|
||||
"web",
|
||||
"Tilman Weber",
|
||||
"webparham",
|
||||
"Wesley",
|
||||
|
|
@ -2148,6 +2197,7 @@
|
|||
"xxonx8",
|
||||
"Ajeet yadav",
|
||||
"Taha yaghobi",
|
||||
"Taha yaghoubi",
|
||||
"yahya",
|
||||
"Arda Yalın",
|
||||
"Yamir",
|
||||
|
|
@ -2227,12 +2277,14 @@
|
|||
"zPanxo",
|
||||
"ZpeedTube",
|
||||
"Krivoy Zub",
|
||||
"Zuro0",
|
||||
"Zwizard",
|
||||
"zx4571",
|
||||
"|_Jenqa_|",
|
||||
"¥¥S.A.N.A¥",
|
||||
"GURLER Çeviri",
|
||||
"Éleftheros",
|
||||
"Berat Öztürk",
|
||||
"Danijel Ćelić",
|
||||
"Štěpán",
|
||||
"Cristian Țicu",
|
||||
|
|
@ -2443,6 +2495,7 @@
|
|||
"전감호",
|
||||
"BombsquadKorea 네이버 카페",
|
||||
"F҉a҉d҉l҉i҉n҉e҉t҉",
|
||||
"Gravöx",
|
||||
"Zona-BombSquad",
|
||||
"༺Leͥgeͣnͫd༻",
|
||||
"CrazySquad",
|
||||
|
|
|
|||
2
dist/ba_data/data/languages/arabic.json
vendored
2
dist/ba_data/data/languages/arabic.json
vendored
|
|
@ -1639,6 +1639,7 @@
|
|||
"Indonesian": "الأندونيسية",
|
||||
"Italian": "الإيطالية",
|
||||
"Japanese": "اليابانية",
|
||||
"Kazakh": "الكازاخية",
|
||||
"Korean": "الكورية",
|
||||
"Malay": "لغة الملايو",
|
||||
"Persian": "الفارسية",
|
||||
|
|
@ -1772,6 +1773,7 @@
|
|||
"You got an achievement reward!": "لقد حصلت على جائزة إنجاز!",
|
||||
"You have been promoted to a new league; congratulations!": "لقد تم ترقيتك إلى الدوري الجديد. تهانينا!",
|
||||
"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 to a newer version of the app to do this.": "يجب تحديث إلى إصدار أحدث من التطبيق للقيام بذلك.",
|
||||
"You must update to the newest version of the game to do this.": "يجب عليك التحديث إلى الإصدار الأحدث من اللعبة للقيام بذلك.",
|
||||
|
|
|
|||
9
dist/ba_data/data/languages/belarussian.json
vendored
9
dist/ba_data/data/languages/belarussian.json
vendored
|
|
@ -1635,7 +1635,8 @@
|
|||
"Arabic": "Арабскi",
|
||||
"Belarussian": "Беларуская",
|
||||
"Chinese": "Кітайская спрошчаная",
|
||||
"ChineseTraditional": "Кітайская традыцыйная",
|
||||
"ChineseSimplified": "Кітайская - Спрошчаная",
|
||||
"ChineseTraditional": "Кітайская - Традыцыіная",
|
||||
"Croatian": "Харвацкая",
|
||||
"Czech": "Чэшская",
|
||||
"Danish": "Дацкая",
|
||||
|
|
@ -1646,7 +1647,7 @@
|
|||
"Finnish": "Фінская",
|
||||
"French": "Французская",
|
||||
"German": "Нямецкая",
|
||||
"Gibberish": "Gibberish",
|
||||
"Gibberish": "Брэхталі",
|
||||
"Greek": "Грэчаскі",
|
||||
"Hindi": "Хіндзі",
|
||||
"Hungarian": "Венгерская",
|
||||
|
|
@ -1659,11 +1660,15 @@
|
|||
"PirateSpeak": "Пірацкая мова",
|
||||
"Polish": "Польская",
|
||||
"Portuguese": "Партугальская",
|
||||
"PortugueseBrazil": "Партугальская - Бразілія",
|
||||
"PortuguesePortugal": "Партугальская - Партугалія",
|
||||
"Romanian": "Румынская",
|
||||
"Russian": "Руская",
|
||||
"Serbian": "Сербская",
|
||||
"Slovak": "Славацкая",
|
||||
"Spanish": "Гішпанская",
|
||||
"SpanishLatinAmerica": "Іспанская - Лацінская Амерыка",
|
||||
"SpanishSpain": "Іспанская - Іспанія",
|
||||
"Swedish": "Шведская",
|
||||
"Tamil": "тамільская",
|
||||
"Thai": "Тайская мова",
|
||||
|
|
|
|||
|
|
@ -1444,7 +1444,7 @@
|
|||
"tokenPack2Text": "中型炸币包",
|
||||
"tokenPack3Text": "大型炸币包",
|
||||
"tokenPack4Text": "超大炸币包",
|
||||
"tokensDescriptionText": "代币用于加速胸部解锁以及其他游戏和帐户功能。\n您可以在游戏中赢得代币,\n\n也可以打包购买。\n或者购买无限代币的Gold Pass,\n再也不会听说它们了。",
|
||||
"tokensDescriptionText": "代币用于加速解锁宝箱以及其他游戏和帐户功能。\n您可以在游戏中赢得代币,\n\n也可以打包购买。\n或者购买无限代币的Gold Pass,\n再也不会听说它们了。",
|
||||
"youHaveGoldPassText": "你获得了黄金通行证\n所有花销全部免费啦!\n感谢你游玩本游戏!"
|
||||
},
|
||||
"topFriendsText": "最佳好友",
|
||||
|
|
@ -1621,6 +1621,7 @@
|
|||
"Indonesian": "印尼语",
|
||||
"Italian": "意大利语",
|
||||
"Japanese": "日本语",
|
||||
"Kazakh": "哈萨克语",
|
||||
"Korean": "朝鲜语",
|
||||
"Malay": "马来语",
|
||||
"Persian": "波斯文",
|
||||
|
|
|
|||
|
|
@ -374,14 +374,14 @@
|
|||
"chatUnMuteText": "取消屏蔽消息",
|
||||
"chests": {
|
||||
"prizeOddsText": "中獎機率",
|
||||
"reduceWaitText": "減少等待",
|
||||
"reduceWaitText": "減少等待時間",
|
||||
"slotDescriptionText": "這個插槽可以容納一個箱子。\n\n透過玩戰役關卡贏取寶箱,\n參加比賽,並完成\n成就。",
|
||||
"slotText": "寶箱槽 ${NUM}",
|
||||
"slotsFullWarningText": "警告:您的所有寶箱槽都已滿。\n您在本遊戲中獲得的所有寶箱都將遺失。",
|
||||
"unlocksInText": "解鎖"
|
||||
"unlocksInText": "距離解鎖剩餘"
|
||||
},
|
||||
"choosingPlayerText": "<選擇玩家>",
|
||||
"claimText": "宣稱",
|
||||
"claimText": "領取",
|
||||
"codesExplainText": "代碼由開發者提供\n以診斷及改正帳戶問題。",
|
||||
"completeThisLevelToProceedText": "你需要先完成\n這一關",
|
||||
"completionBonusText": "完成獎勵",
|
||||
|
|
@ -1072,9 +1072,9 @@
|
|||
"merchText": "周邊",
|
||||
"modeArcadeText": "街機模式",
|
||||
"modeClassicText": "經典模式",
|
||||
"modeDemoText": "演示模式",
|
||||
"modeDemoText": "示範模式",
|
||||
"moreSoonText": "更多內容即將推出...",
|
||||
"mostDestroyedPlayerText": "被摧毀次數最多的球員",
|
||||
"mostDestroyedPlayerText": "最遭受擊殺的玩家",
|
||||
"mostValuablePlayerText": "最有價值的玩家",
|
||||
"mostViolatedPlayerText": "最遭受暴力的玩家",
|
||||
"mostViolentPlayerText": "最暴力的玩家",
|
||||
|
|
@ -1123,7 +1123,7 @@
|
|||
"okText": "好的",
|
||||
"onText": "開",
|
||||
"oneMomentText": "請等待",
|
||||
"onslaughtRespawnText": "${PLAYER}將於${WAVE}波復活",
|
||||
"onslaughtRespawnText": "${PLAYER}將於第${WAVE}波復活",
|
||||
"openMeText": "打開我!",
|
||||
"openNowText": "即時打開",
|
||||
"openText": "打開",
|
||||
|
|
@ -1452,7 +1452,7 @@
|
|||
"getTokensText": "獲得代幣...",
|
||||
"notEnoughTokensText": "代幣不足!",
|
||||
"numTokensText": "${COUNT}代幣",
|
||||
"openNowDescriptionText": "您有足夠的代幣\n現在打開這個 - 你不\n需要等待。",
|
||||
"openNowDescriptionText": "您有足夠的代幣去\n即時打開這寶箱\n毋需等待",
|
||||
"shinyNewCurrencyText": "炸彈小分隊 閃亮亮 的全新遊戲幣!!",
|
||||
"tokenPack1Text": "50代幣",
|
||||
"tokenPack2Text": "500代幣",
|
||||
|
|
@ -1616,6 +1616,7 @@
|
|||
"Arabic": "阿拉伯語",
|
||||
"Belarussian": "白俄羅斯語",
|
||||
"Chinese": "簡體中文",
|
||||
"ChineseSimplified": "簡體中文",
|
||||
"ChineseTraditional": "繁體中文",
|
||||
"Croatian": "克羅地亞語",
|
||||
"Czech": "捷克語",
|
||||
|
|
@ -1634,17 +1635,22 @@
|
|||
"Indonesian": "印尼語",
|
||||
"Italian": "意大利語",
|
||||
"Japanese": "日語",
|
||||
"Kazakh": "哈薩克語",
|
||||
"Korean": "朝鮮語",
|
||||
"Malay": "馬來語",
|
||||
"Persian": "波斯文",
|
||||
"PirateSpeak": "海盜口語",
|
||||
"Polish": "波蘭語",
|
||||
"Portuguese": "葡萄牙語",
|
||||
"PortugueseBrazil": "葡萄牙語(巴西)",
|
||||
"PortuguesePortugal": "葡萄牙語(葡萄牙)",
|
||||
"Romanian": "羅馬尼亞語",
|
||||
"Russian": "俄羅斯語",
|
||||
"Serbian": "塞爾維亞語",
|
||||
"Slovak": "斯洛伐克語",
|
||||
"Spanish": "西班牙語",
|
||||
"SpanishLatinAmerica": "西班牙語(拉丁美洲)",
|
||||
"SpanishSpain": "西班牙語(西班牙)",
|
||||
"Swedish": "瑞典語",
|
||||
"Tamil": "泰米爾語",
|
||||
"Thai": "泰語",
|
||||
|
|
@ -1943,7 +1949,7 @@
|
|||
"validatingTestBuildText": "測試版驗證中",
|
||||
"viaText": "其他賬戶",
|
||||
"victoryText": "勝利!",
|
||||
"voteDelayText": "你不能在${NUMBER}內發起一個新的投票",
|
||||
"voteDelayText": "你不能在${NUMBER}秒內發起一個新的投票",
|
||||
"voteInProgressText": "已經有一個投票正在進行中了",
|
||||
"votedAlreadyText": "你已經參與過投票了",
|
||||
"votesNeededText": "通過需要${NUMBER}個投票",
|
||||
|
|
|
|||
8
dist/ba_data/data/languages/czech.json
vendored
8
dist/ba_data/data/languages/czech.json
vendored
|
|
@ -1639,7 +1639,8 @@
|
|||
"Arabic": "Arabština",
|
||||
"Belarussian": "Běloruština",
|
||||
"Chinese": "Zjednodušená Čínština",
|
||||
"ChineseTraditional": "Tradiční Čínština",
|
||||
"ChineseSimplified": "Čínština - Zjednodušená",
|
||||
"ChineseTraditional": "Čínština - Tradiční",
|
||||
"Croatian": "Chorvatština",
|
||||
"Czech": "Čeština",
|
||||
"Danish": "Dánština",
|
||||
|
|
@ -1657,17 +1658,22 @@
|
|||
"Indonesian": "Indonéština",
|
||||
"Italian": "Italština",
|
||||
"Japanese": "Japonština",
|
||||
"Kazakh": "Kazaština",
|
||||
"Korean": "Korejština",
|
||||
"Malay": "Malajština",
|
||||
"Persian": "Perština",
|
||||
"PirateSpeak": "Řeč pirátů",
|
||||
"Polish": "Polština",
|
||||
"Portuguese": "Portugalština",
|
||||
"PortugueseBrazil": "Portugalština - Brazilská",
|
||||
"PortuguesePortugal": "Portugalština - Portugalská",
|
||||
"Romanian": "Rumunština",
|
||||
"Russian": "Ruština",
|
||||
"Serbian": "Srbština",
|
||||
"Slovak": "Slovenština",
|
||||
"Spanish": "Španělština",
|
||||
"SpanishLatinAmerica": "Španělština - Latinsko Americká",
|
||||
"SpanishSpain": "Španělština - Španělská",
|
||||
"Swedish": "Švédština",
|
||||
"Tamil": "Tamiština",
|
||||
"Thai": "Thajština",
|
||||
|
|
|
|||
276
dist/ba_data/data/languages/danish.json
vendored
276
dist/ba_data/data/languages/danish.json
vendored
|
|
@ -1,50 +1,60 @@
|
|||
{
|
||||
"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)",
|
||||
"accountsText": "Konti",
|
||||
"achievementProgressText": "Achievements: ${COUNT} ud af ${TOTAL}",
|
||||
"achievementProgressText": "Præstationer: ${COUNT} ud af ${TOTAL}",
|
||||
"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)",
|
||||
"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",
|
||||
"linkAccountsGenerateCodeText": "Generér kode",
|
||||
"linkAccountsInfoText": "(del forløb 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.",
|
||||
"linkAccountsInfoText": "(del fremskridt på tværs af platforme)",
|
||||
"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!",
|
||||
"linkAccountsText": "Forbind konti",
|
||||
"linkedAccountsText": "Forbundne konti:",
|
||||
"nameChangeConfirm": "Ændre dit konto navn til ${NAME}?",
|
||||
"manageAccountText": "Administrer konti",
|
||||
"nameChangeConfirm": "Skift dit kontonavn til ${NAME}?",
|
||||
"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?",
|
||||
"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?",
|
||||
"resetProgressText": "Nulstil Process",
|
||||
"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,\npræstationer og lokale highscores. \n(men ikke dine billetter). Dette kan ikke \ngøres om. Er du sikker?",
|
||||
"resetProgressText": "Nulstil fremskridt",
|
||||
"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.",
|
||||
"signInInfoText": "Log in for at optjene tickets, konkurrere online \nog dele dit forløb på tværs af enheder.",
|
||||
"signInText": "Log Ind",
|
||||
"signInWithDeviceInfoText": "(en automatisk oprettet konto kun tilgængelig fra denne enhed)",
|
||||
"signInWithDeviceText": "Log in med en enheds-konto.",
|
||||
"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 ind for at optjene billetter, konkurrere online \nog dele dit forløb på tværs af enheder.",
|
||||
"signInText": "Log ind",
|
||||
"signInWithAnEmailAddressText": "Log ind med email",
|
||||
"signInWithDeviceInfoText": "(en automatisk oprettet konto er kun tilgængelig fra denne enhed)",
|
||||
"signInWithDeviceText": "Log ind med en enhedskonto",
|
||||
"signInWithGameCircleText": "Log in med Game Circle",
|
||||
"signInWithGooglePlayText": "Log in med Google Play",
|
||||
"signInWithTestAccountInfoText": "(ældre kontotype; brug enheds-konti fremover)",
|
||||
"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",
|
||||
"signingInText": "Logger ind...",
|
||||
"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!)",
|
||||
"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}",
|
||||
"titleText": "Din bruger",
|
||||
"unlinkAccountsInstructionsText": "Vælg en konto at adskille",
|
||||
"unlinkAccountsText": "Adskil kontoer",
|
||||
"ticketsText": "Billetter: ${COUNT}",
|
||||
"titleText": "Konto",
|
||||
"unlinkAccountsInstructionsText": "Vælg den konto du vil adskille",
|
||||
"unlinkAccountsText": "Adskil konti",
|
||||
"unlinkLegacyV1AccountsText": "Adskil Legacy (V1) konti",
|
||||
"v2LinkInstructionsText": "Brug dette link for at oprette en konto eller logge ind.",
|
||||
"viaAccount": "(via konto ${NAME})",
|
||||
"youAreLoggedInAsText": "Du er logget ind som: ",
|
||||
"youAreSignedInAsText": "Du er logget ind som:"
|
||||
},
|
||||
"achievementChallengesText": "Achievementudfordringer",
|
||||
"achievementText": "Achievement",
|
||||
"achievementChallengesText": "Præstationsudfordringer",
|
||||
"achievementText": "Præstation",
|
||||
"achievements": {
|
||||
"Boom Goes the Dynamite": {
|
||||
"description": "Dræb 3 fjender med dynamit",
|
||||
|
|
@ -62,8 +72,8 @@
|
|||
},
|
||||
"Dual Wielding": {
|
||||
"descriptionFull": "Forbind 2 kontrollere (hardware eller app)",
|
||||
"descriptionFullComplete": "2 kontrollere forbundet (hardware eller app)",
|
||||
"name": "Dobbelt-båren"
|
||||
"descriptionFullComplete": "2 kontrollere er forbundet (hardware eller app)",
|
||||
"name": "Dobbelt våbenføring"
|
||||
},
|
||||
"Flawless Victory": {
|
||||
"description": "Vind uden at blive ramt",
|
||||
|
|
@ -93,13 +103,13 @@
|
|||
},
|
||||
"In Control": {
|
||||
"descriptionFull": "Forbind en controller (hardware eller app)",
|
||||
"descriptionFullComplete": "Forbind en controller. (hardware eller app)",
|
||||
"name": "Under Kontrol"
|
||||
"descriptionFullComplete": "En controller forbundet. (hardware eller app)",
|
||||
"name": "Under kontrol"
|
||||
},
|
||||
"Last Stand God": {
|
||||
"description": "Scor 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}",
|
||||
"name": "Gud i ${LEVEL}"
|
||||
},
|
||||
|
|
@ -126,7 +136,7 @@
|
|||
},
|
||||
"Off You Go Then": {
|
||||
"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}",
|
||||
"descriptionFullComplete": "Du kastede 3 fjender af banen i ${LEVEL}",
|
||||
"name": "Afsted med dig"
|
||||
|
|
@ -139,7 +149,7 @@
|
|||
"name": "Gud i ${LEVEL}"
|
||||
},
|
||||
"Onslaught Master": {
|
||||
"description": "Scor 5000 point",
|
||||
"description": "Scor 500 point",
|
||||
"descriptionComplete": "Du scorede 500 point",
|
||||
"descriptionFull": "Scor 500 point i ${LEVEL}",
|
||||
"descriptionFullComplete": "Du scorede 500 point i ${LEVEL}",
|
||||
|
|
@ -178,7 +188,7 @@
|
|||
"descriptionComplete": "Du vandt uden at lade fjenderne score",
|
||||
"descriptionFull": "Vind ${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": {
|
||||
"description": "Vind kampen",
|
||||
|
|
@ -206,7 +216,7 @@
|
|||
"descriptionComplete": "Du vandt uden at fjenderne scorede",
|
||||
"descriptionFull": "Vind ${LEVEL} uden at fjenderne scorer",
|
||||
"descriptionFullComplete": "Du vandt ${LEVEL} uden at fjenderne scorede",
|
||||
"name": "Straffespark i ${LEVEL}"
|
||||
"name": "Sejr uden mål i ${LEVEL}"
|
||||
},
|
||||
"Rookie Football Victory": {
|
||||
"description": "Vind kampen",
|
||||
|
|
@ -246,7 +256,7 @@
|
|||
"Sharing is Caring": {
|
||||
"descriptionFull": "Del spillet med en ven",
|
||||
"descriptionFullComplete": "Har delt spillet med en ven",
|
||||
"name": "Sharing is Caring"
|
||||
"name": "Deling er heling"
|
||||
},
|
||||
"Stayin' Alive": {
|
||||
"description": "Vind uden at dø",
|
||||
|
|
@ -267,40 +277,40 @@
|
|||
"descriptionComplete": "Du gav 50% skade med ét slag",
|
||||
"descriptionFull": "Giv 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": {
|
||||
"description": "Dræb 6 fjender med TNT",
|
||||
"descriptionComplete": "Du dræbte 6 fjender med TNT",
|
||||
"descriptionFull": "Dræb 6 fjender med TNT i ${LEVEL}",
|
||||
"descriptionFullComplete": "Du dræbte 6 fjender med TNT i ${LEVEL}",
|
||||
"name": "TNT-terror"
|
||||
"name": "TNT Terror"
|
||||
},
|
||||
"Team Player": {
|
||||
"descriptionFull": "Start et Hold spil med mere end 4 spillere",
|
||||
"descriptionFullComplete": "Startede et Hold spil med mere end 4 spillere",
|
||||
"name": "Hold Spiller"
|
||||
"descriptionFull": "Start et holdspil med mere end 4 spillere",
|
||||
"descriptionFullComplete": "Startede et holdspil med mere end 4 spillere",
|
||||
"name": "Holdspiller"
|
||||
},
|
||||
"The Great Wall": {
|
||||
"description": "Stop hver eneste fjende",
|
||||
"descriptionComplete": "Du stoppede hver eneste fjende",
|
||||
"descriptionFull": "Stop hver eneste fjende i ${LEVEL}",
|
||||
"descriptionFullComplete": "Du stoppede hver eneste fjende i ${LEVEL}",
|
||||
"description": "Stop alle fjender",
|
||||
"descriptionComplete": "Du stoppede alle fjender",
|
||||
"descriptionFull": "Stop alle fjender i ${LEVEL}",
|
||||
"descriptionFullComplete": "Du stoppede alle fjender i ${LEVEL}",
|
||||
"name": "Den Store Mur"
|
||||
},
|
||||
"The Wall": {
|
||||
"description": "Stop hver eneste fjende",
|
||||
"descriptionComplete": "Du stoppede hver eneste fjende",
|
||||
"descriptionFull": "Stop hver eneste fjende i ${LEVEL}",
|
||||
"descriptionFullComplete": "Du stoppede hver eneste fjende i ${LEVEL}",
|
||||
"description": "Stop alle fjender",
|
||||
"descriptionComplete": "Du stoppede alle fjender",
|
||||
"descriptionFull": "Stop alle fjender i ${LEVEL}",
|
||||
"descriptionFullComplete": "Du stoppede alle fjender i ${LEVEL}",
|
||||
"name": "Muren"
|
||||
},
|
||||
"Uber Football Shutout": {
|
||||
"description": "Vind uden at fjenderne scorer",
|
||||
"description": "Vind uden at fjenden scorer",
|
||||
"descriptionComplete": "Du vandt uden at fjenden scorede",
|
||||
"descriptionFull": "Vind ${LEVEL} uden at fjenden scorer",
|
||||
"descriptionFullComplete": "Du vandt ${LEVEL} uden at fjenden scorede",
|
||||
"name": "Straffespark i ${LEVEL}"
|
||||
"name": "Sejr uden mål i ${LEVEL}"
|
||||
},
|
||||
"Uber Football Victory": {
|
||||
"description": "Vind kampen",
|
||||
|
|
@ -324,20 +334,26 @@
|
|||
"name": "Sejr i ${LEVEL}"
|
||||
}
|
||||
},
|
||||
"achievementsRemainingText": "Achievements tilbage:",
|
||||
"achievementsText": "Achievements",
|
||||
"achievementsUnavailableForOldSeasonsText": "Beklager, det er ikke muligt at se detaljer for tidligere sæsoner.",
|
||||
"achievementsRemainingText": "Præstationer tilbage:",
|
||||
"achievementsText": "Præstationer",
|
||||
"achievementsUnavailableForOldSeasonsText": "Beklager, det er ikke muligt at se præstationer for tidligere sæsoner.",
|
||||
"activatedText": "${THING} aktiveret.",
|
||||
"addGameWindow": {
|
||||
"getMoreGamesText": "Få Flere Spil...",
|
||||
"getMoreGamesText": "Få flere spil...",
|
||||
"titleText": "Tilføj spil"
|
||||
},
|
||||
"addToFavoritesText": "Tilføj til favoritter",
|
||||
"addedToFavoritesText": "Tilføjede '${NAME}' to favoritter.",
|
||||
"allText": "Alt",
|
||||
"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",
|
||||
"apiVersionErrorText": "Kan ikke indlæse ${NAME}; det er målrettet api-version ${VERSION_USED}; vi behøver ${VERSION_REQUIRED}.",
|
||||
"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 modulet ${NAME}; det er målrettet api-version ${VERSION_USED}; vi kræver ${VERSION_REQUIRED}.",
|
||||
"applyText": "Tilføj",
|
||||
"areYouSureText": "Er du sikker?",
|
||||
"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",
|
||||
"musicVolumeText": "Musiklydstyrke",
|
||||
"musicVolumeText": "Lydstyrke musik",
|
||||
"soundVolumeText": "Lydstyrke",
|
||||
"soundtrackButtonText": "Lydspor",
|
||||
"soundtrackDescriptionText": "(Afspil din egen musik, mens du spiller)",
|
||||
|
|
@ -345,11 +361,11 @@
|
|||
},
|
||||
"autoText": "Auto",
|
||||
"backText": "Tilbage",
|
||||
"banThisPlayerText": "Ban denne spiller",
|
||||
"banThisPlayerText": "Bloker denne spiller",
|
||||
"bestOfFinalText": "Bedst ud af ${COUNT} finale",
|
||||
"bestOfSeriesText": "Bedst ud af ${COUNT}:",
|
||||
"bestRankText": "Dine bedste er #${RANK}",
|
||||
"bestRatingText": "Din bedste vurdering er ${RATING}",
|
||||
"bestRankText": "Din bedste er #${RANK}",
|
||||
"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",
|
||||
"betaValidateErrorText": "Ude af stand til til at validere beta. Har du tjekket din internetforbindelse?",
|
||||
"betaValidatedText": "Beta valideret; Nyd den!",
|
||||
|
|
@ -358,15 +374,25 @@
|
|||
"boostText": "Boost",
|
||||
"bsRemoteConfigureInAppText": "${REMOTE_APP_NAME} konfigureres i selve app'en.",
|
||||
"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",
|
||||
"cantConfigureDeviceText": "Undskyld, ${DEVICE} kan ikke konfigureres.",
|
||||
"challengeEndedText": "Udfordringen er afsluttet.",
|
||||
"cantConfigureDeviceText": "${DEVICE} kan ikke konfigureres.",
|
||||
"challengeEndedText": "Denne udfordring er afsluttet.",
|
||||
"chatMuteText": "Slå chat fra",
|
||||
"chatMutedText": "Chat fra",
|
||||
"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>",
|
||||
"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",
|
||||
"configControllersWindow": {
|
||||
"configureControllersText": "Konfigurer Controllers",
|
||||
|
|
@ -378,12 +404,12 @@
|
|||
"ps3Text": "PS3-controllere",
|
||||
"titleText": "Controllere",
|
||||
"wiimotesText": "Wiimotes",
|
||||
"xbox360Text": "Xbox 360-controllere"
|
||||
"xbox360Text": "Xbox 360 controllere"
|
||||
},
|
||||
"configGamepadSelectWindow": {
|
||||
"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.",
|
||||
"titleText": "Konfigurér Controllers"
|
||||
"pressAnyButtonText": "Tryk på en vilkårlig knap på controlleren\nsom du gerne vil konfigurere...",
|
||||
"titleText": "Konfigurér Controllere"
|
||||
},
|
||||
"configGamepadWindow": {
|
||||
"advancedText": "Avanceret",
|
||||
|
|
@ -417,7 +443,7 @@
|
|||
"runTrigger1Text": "Løbeudløser 1",
|
||||
"runTrigger2Text": "Løbeudløser 2",
|
||||
"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",
|
||||
"secondaryText": "Sekundær Controller",
|
||||
"startButtonActivatesDefaultDescriptionText": "(sluk denne, hvis din 'start'-knap fungerer mere som en 'menu'-knap)",
|
||||
|
|
@ -426,14 +452,14 @@
|
|||
"twoInOneSetupText": "2-i-1 controllersetup",
|
||||
"uiOnlyDescriptionText": "(forhindre, at denne controller deltager i et spil)",
|
||||
"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>",
|
||||
"vrReorientButtonText": "VR Positionsnulstillings Knap"
|
||||
},
|
||||
"configKeyboardWindow": {
|
||||
"configuringText": "Konfigurerer ${DEVICE}",
|
||||
"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": {
|
||||
"actionControlScaleText": "Action kontrol vægt",
|
||||
|
|
@ -444,19 +470,20 @@
|
|||
"movementControlScaleText": "Bevægelseskontrol vægt",
|
||||
"movementText": "Bevægelse",
|
||||
"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.",
|
||||
"swipeText": "swipe",
|
||||
"titleText": "Konfigurér touchskærm",
|
||||
"touchControlsScaleText": "Skala ift. berøringsstyring"
|
||||
},
|
||||
"configureDeviceInSystemSettingsText": "${DEVICE} kan konfigureres i System Indstillings app'en.",
|
||||
"configureItNowText": "Konfigurér den nu?",
|
||||
"configureText": "Konfigurér",
|
||||
"connectMobileDevicesWindow": {
|
||||
"amazonText": "Amazon Appstore",
|
||||
"appStoreText": "App Store",
|
||||
"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,
|
||||
"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:",
|
||||
|
|
@ -469,11 +496,11 @@
|
|||
"continueText": "Fortsæt",
|
||||
"controlsText": "Styring",
|
||||
"coopSelectWindow": {
|
||||
"activenessAllTimeInfoText": "Nem mindenki Joshua",
|
||||
"activenessInfoText": "Denne multiplikator siger på dage hvor du\nspiller og falder på dage hvor du ikke spiller.",
|
||||
"activenessAllTimeInfoText": "Dette medregnes ikke ved all-time rangering.",
|
||||
"activenessInfoText": "Denne multiplikator stiger på dage hvor du\nspiller og falder på dage hvor du ikke spiller.",
|
||||
"activityText": "Aktivitet",
|
||||
"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",
|
||||
"currentBestText": "Nuværende bedste",
|
||||
"customText": "Brugerdefineret",
|
||||
|
|
@ -495,7 +522,7 @@
|
|||
"powerRankingPointsToRankedText": "(${CURRENT} ud af ${REMAINING} point)",
|
||||
"powerRankingText": "Power Rangering",
|
||||
"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...",
|
||||
"skipWaitText": "Skip at vente",
|
||||
"timeRemainingText": "Tid tilbage",
|
||||
|
|
@ -504,14 +531,15 @@
|
|||
"totalText": "i alt",
|
||||
"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.",
|
||||
"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.",
|
||||
"yourPowerRankingText": "Din Power Rangering"
|
||||
"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:"
|
||||
},
|
||||
"copyConfirmText": "Kopier til udklipsholder.",
|
||||
"copyOfText": "${NAME} kopi",
|
||||
"copyText": "Kopier",
|
||||
"copyrightText": "© 2013 Eric Froemling",
|
||||
"createAPlayerProfileText": "Opret en spillerprofil?",
|
||||
"createEditPlayerText": "<Lav/Rediger Spiller>",
|
||||
"createEditPlayerText": "<Opret/Rediger Spiller>",
|
||||
"createText": "Opret",
|
||||
"creditsWindow": {
|
||||
"additionalAudioArtIdeasText": "Ekstra lyd, tidlige illustrationer og idéer af ${NAME}",
|
||||
|
|
@ -552,7 +580,7 @@
|
|||
"unlockCoopText": "Åbn co-op levels"
|
||||
},
|
||||
"defaultFreeForAllGameListNameText": "Standard alle mod alle-spil",
|
||||
"defaultGameListNameText": "Standard ${PLAYMODE} spilleliste",
|
||||
"defaultGameListNameText": "Standard ${PLAYMODE} Playliste",
|
||||
"defaultNewFreeForAllGameListNameText": "Mine alle mod alle-spil",
|
||||
"defaultNewGameListNameText": "Min ${PLAYMODE} playliste",
|
||||
"defaultNewTeamGameListNameText": "Mine holdspil",
|
||||
|
|
@ -560,20 +588,27 @@
|
|||
"deleteText": "Slet",
|
||||
"demoText": "Demo",
|
||||
"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",
|
||||
"difficultyHardOnlyText": "Svær niveautilstand udelukkende",
|
||||
"difficultyHardText": "Svær",
|
||||
"difficultyHardUnlockOnlyText": "Dette niveau kan kun blive oplåst på svært niveautilstand.\nTror du at du har hvad der skal til!?!?!?",
|
||||
"difficultyHardOnlyText": "Kun svært niveau",
|
||||
"difficultyHardText": "Svært",
|
||||
"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:",
|
||||
"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",
|
||||
"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",
|
||||
"drawText": "Uafgjort",
|
||||
"duplicateText": "Dupliker",
|
||||
"duplicateText": "Kopier",
|
||||
"editGameListWindow": {
|
||||
"addGameText": "Tilføj spil",
|
||||
"addGameText": "Tilføj\nspil",
|
||||
"cantOverwriteDefaultText": "Kan ikke overskrive standard spilliste!",
|
||||
"cantSaveAlreadyExistsText": "En spilleliste med det navn eksisterer allerede!",
|
||||
"cantSaveEmptyListText": "En tom spilleliste kan ikke gemmes!",
|
||||
|
|
@ -586,12 +621,12 @@
|
|||
"titleText": "Spilleliste editor"
|
||||
},
|
||||
"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)",
|
||||
"availableText": "Dette navn \"${NAME}\" er ledigt.",
|
||||
"changesNotAffectText": "Bemærk: ændringer vil ikke have nogen indvirkning på spillere, som allerede er i spil.",
|
||||
"characterText": "karakter",
|
||||
"checkingAvailabilityText": "Checker muligheden for \"${NAME}\"...",
|
||||
"checkingAvailabilityText": "Undersøger muligheden for \"${NAME}\"...",
|
||||
"colorText": "farve",
|
||||
"getMoreCharactersText": "Få flere karakterer...",
|
||||
"getMoreIconsText": "Få flere ikoner...",
|
||||
|
|
@ -603,6 +638,7 @@
|
|||
"localProfileText": "(lokal profil)",
|
||||
"nameDescriptionText": "Spillernavn",
|
||||
"nameText": "Navn",
|
||||
"profileAlreadyExistsText": "En profil med det navn eksisterer allerede.",
|
||||
"randomText": "tilfældig",
|
||||
"titleEditText": "Rediger profil",
|
||||
"titleNewText": "Ny profil",
|
||||
|
|
@ -624,7 +660,7 @@
|
|||
"duplicateText": "Dupliker\nLydspor",
|
||||
"editSoundtrackText": "Lydspor Editor",
|
||||
"editText": "Rediger\nLydspor",
|
||||
"fetchingITunesText": "Henter iTunes spilleliste...",
|
||||
"fetchingITunesText": "henter Musik App spilleliste...",
|
||||
"musicVolumeZeroWarning": "Advarsel: Din lydstyrke i spillet er sat til 0",
|
||||
"nameText": "Navn",
|
||||
"newSoundtrackNameText": "Mit lydspor ${COUNT}",
|
||||
|
|
@ -636,57 +672,65 @@
|
|||
"testText": "Hør",
|
||||
"titleText": "Lydspor",
|
||||
"useDefaultGameMusicText": "Standard spilmusik",
|
||||
"useITunesPlaylistText": "iTunesplayliste",
|
||||
"useITunesPlaylistText": "Musik App Playliste",
|
||||
"useMusicFileText": "Musikfil (mp3 osv)",
|
||||
"useMusicFolderText": "Mappe med musikfiler"
|
||||
},
|
||||
"editText": "Rediger",
|
||||
"enabledText": "Aktiveret",
|
||||
"endText": "Afslut",
|
||||
"enjoyText": "God fornøjelse!",
|
||||
"epicDescriptionFilterText": "${DESCRIPTION} i imponerende slowmotion.",
|
||||
"epicNameFilterText": "Imponerende ${NAME}",
|
||||
"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",
|
||||
"errorSecureConnectionFailText": "Det er ikke muligt at lave en sikker cloud forbindelse; netværksfunktionalitet vil måske fejle.",
|
||||
"errorText": "Fejl",
|
||||
"errorUnknownText": "ukendt fejl",
|
||||
"exitGameText": "Afslut ${APP_NAME}?",
|
||||
"expiredAgoText": "Udløb for ${T} siden",
|
||||
"expiresInText": "Udløber om ${T}",
|
||||
"exportSuccessText": "'${NAME}' eksporteret.",
|
||||
"externalStorageText": "Ekstern lagring",
|
||||
"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": {
|
||||
"titleFileFolderText": "Vælg en fil eller mappe",
|
||||
"titleFileText": "Vælg en fil",
|
||||
"titleFolderText": "Vælg en mappe",
|
||||
"useThisFolderButtonText": "Brug denne mappe"
|
||||
},
|
||||
"filterText": "Filtrer",
|
||||
"finalScoreText": "Endelig score",
|
||||
"finalScoresText": "Endelige scorer",
|
||||
"finalTimeText": "Endelig tid",
|
||||
"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.",
|
||||
"firstToFinalText": "Først til ${COUNT} – Finale",
|
||||
"firstToSeriesText": "Første til ${COUNT}",
|
||||
"fiveKillText": "FEM DRAB!!!!",
|
||||
"firstToFinalText": "Første-til-${COUNT} Finale",
|
||||
"firstToSeriesText": "Første-til-${COUNT} serie",
|
||||
"fiveKillText": "FEM DRAB!!!",
|
||||
"flawlessWaveText": "Fejlfri bølge!",
|
||||
"fourKillText": "FIREDOBBELT DRAB!!!!",
|
||||
"fourKillText": "FIREDOBBELT DRAB!!!",
|
||||
"freeForAllText": "Alle mod alle",
|
||||
"friendScoresUnavailableText": "Dine venners scorer er utilgængelige.",
|
||||
"gameCenterText": "GameCenter",
|
||||
"gameCircleText": "GameCircle",
|
||||
"gameLeadersText": "Stilling efter ${COUNT}. spil:",
|
||||
"gameLeadersText": "Stilling efter ${COUNT} spil",
|
||||
"gameListWindow": {
|
||||
"cantDeleteDefaultText": "Du kan ikke slette standard spilliste!",
|
||||
"cantEditDefaultText": "Kan ikke redigere i standardspillisten! Dupliker den eller lav en ny.",
|
||||
"cantShareDefaultText": "Du kan ikke dele den standard spilliste.",
|
||||
"cantDeleteDefaultText": "Du kan ikke slette standard spillisten.",
|
||||
"cantEditDefaultText": "Kan ikke redigere i standard spillisten! Kopier den eller opret en ny.",
|
||||
"cantShareDefaultText": "Du kan ikke dele standard spillisten.",
|
||||
"deleteConfirmText": "Slet \"${LIST}\"?",
|
||||
"deleteText": "Slet\nSpilliste",
|
||||
"duplicateText": "Dupliker\nSpilliste",
|
||||
"editText": "Rediger\nSpilliste",
|
||||
"gameListText": "Spilliste",
|
||||
"newText": "Ny\nSpilliste",
|
||||
"pointsToWinText": "Point for at vinde",
|
||||
"seriesLengthText": "Serie længde",
|
||||
"showTutorialText": "Vis tutorial",
|
||||
"shuffleGameOrderText": "Bland rækkefølgen",
|
||||
"shuffleGameOrderText": "Bland spilrækkefølgen",
|
||||
"titleText": "Tilpas ${TYPE} Spillister"
|
||||
},
|
||||
"gameSettingsWindow": {
|
||||
|
|
@ -709,18 +753,26 @@
|
|||
"bluetoothJoinText": "Deltag over Bluetooth",
|
||||
"bluetoothText": "Bluetooth",
|
||||
"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.",
|
||||
"descriptionShortText": "Brug saml-vinduet for at samle et hold.",
|
||||
"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)",
|
||||
"earnTicketsForRecommendingText": "Del spiller\nfor gratis billetter...",
|
||||
"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}",
|
||||
"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.",
|
||||
"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.",
|
||||
"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",
|
||||
"googlePlayDescriptionText": "Inviter Google Play spillere til din gruppe:",
|
||||
"googlePlayInviteText": "Inviter",
|
||||
|
|
@ -728,19 +780,21 @@
|
|||
"googlePlaySeeInvitesText": "Se inviterede",
|
||||
"googlePlayText": "Google Play",
|
||||
"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.",
|
||||
"internetText": "Internet",
|
||||
"inviteAFriendText": "Venner har ikke spillet? Inviter de til at\nprøve det og så modtager de ${COUNT} gratis billetter.",
|
||||
"inviteFriendsText": "Inviter venner",
|
||||
"joinPublicPartyDescriptionText": "Deltag i en offentlig gruppe:",
|
||||
"localNetworkDescriptionText": "Deltag i en gruppe på dit netværk:",
|
||||
"joinPublicPartyDescriptionText": "Deltag i en offentlig gruppe",
|
||||
"localNetworkDescriptionText": "Deltag i en gruppe på dit netværk (LAN, Bluetooth, osv.)",
|
||||
"localNetworkText": "Lokalt netværk",
|
||||
"makePartyPrivateText": "Gør min gruppe privat",
|
||||
"makePartyPublicText": "Gør min gruppe offentlig",
|
||||
"manualAddressText": "Adresse",
|
||||
"manualConnectText": "Opret forbindelse",
|
||||
"manualDescriptionText": "Deltag i en gruppe på en adresse:",
|
||||
"manualJoinSectionText": "Deltag med en adresse",
|
||||
"manualJoinableFromInternetText": "Kan du deltage via internettet?:",
|
||||
"manualJoinableNoWithAsteriskText": "NEJ*",
|
||||
"manualJoinableYesText": "JA",
|
||||
|
|
@ -748,14 +802,18 @@
|
|||
"manualText": "Manual",
|
||||
"manualYourAddressFromInternetText": "Din adresse fra internettet:",
|
||||
"manualYourLocalAddressText": "Din lokale adresse:",
|
||||
"nearbyText": "Nær",
|
||||
"noConnectionText": "<ingen forbindelse>",
|
||||
"noPartiesAddedText": "Ingen grupper tilføjet",
|
||||
"otherVersionsText": "(andre versioner)",
|
||||
"partyCodeText": "Gruppekode",
|
||||
"partyInviteAcceptText": "Accepter",
|
||||
"partyInviteDeclineText": "Afvis",
|
||||
"partyInviteGooglePlayExtraText": "(Se 'Google Play' fanen i 'Saml' vinduet)",
|
||||
"partyInviteIgnoreText": "Ignorer",
|
||||
"partyInviteText": "${NAME} har inviteret\ndig til at deltage i deres gruppe!",
|
||||
"partyNameText": "Gruppe navn",
|
||||
"partyServerRunningText": "Din gruppeserver kører.",
|
||||
"partySizeText": "gruppe størrelse",
|
||||
"partyStatusCheckingText": "kontrollerer status...",
|
||||
"partyStatusJoinableText": "din gruppe kan nu tilsluttes fra internettet",
|
||||
|
|
@ -764,10 +822,20 @@
|
|||
"partyStatusNotPublicText": "din gruppe er ikke offentlig",
|
||||
"pingText": "ping",
|
||||
"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...",
|
||||
"sendDirectInvitesText": "Send direkte invitationer",
|
||||
"shareThisCodeWithFriendsText": "Del denne kode med dine venner:",
|
||||
"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",
|
||||
"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.",
|
||||
|
|
@ -797,7 +865,7 @@
|
|||
"ticketPack4Text": "Jumbo billetpakke",
|
||||
"ticketPack5Text": "Mammut billetpakke",
|
||||
"ticketPack6Text": "Ultimativ billetpakke",
|
||||
"ticketsFromASponsorText": "Få ${COUNT} billetter\nfra en sponsor",
|
||||
"ticketsFromASponsorText": "Se en reklame\nfor at få ${COUNT} billetter",
|
||||
"ticketsText": "${COUNT} 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.",
|
||||
|
|
|
|||
5
dist/ba_data/data/languages/dutch.json
vendored
5
dist/ba_data/data/languages/dutch.json
vendored
|
|
@ -1706,6 +1706,7 @@
|
|||
"Arabic": "Arabisch",
|
||||
"Belarussian": "Belarusian",
|
||||
"Chinese": "Vereenvoudigd Chinees ",
|
||||
"ChineseSimplified": "Vereenvoudigd Chinees",
|
||||
"ChineseTraditional": "Traditioneel Chinees",
|
||||
"Croatian": "Kroatisch",
|
||||
"Czech": "Tsjechisch",
|
||||
|
|
@ -1730,11 +1731,15 @@
|
|||
"PirateSpeak": "Piraat Praat",
|
||||
"Polish": "Pools",
|
||||
"Portuguese": "Portugees",
|
||||
"PortugueseBrazil": "Portugees - Brazilië",
|
||||
"PortuguesePortugal": "Portugees - Portugal",
|
||||
"Romanian": "Roemeens",
|
||||
"Russian": "Russisch",
|
||||
"Serbian": "Servisch",
|
||||
"Slovak": "Sloveens",
|
||||
"Spanish": "Spaans",
|
||||
"SpanishLatinAmerica": "Spaans - Latijns Amerika",
|
||||
"SpanishSpain": "Spaans - Spanje",
|
||||
"Swedish": "Zweeds",
|
||||
"Tamil": "Tamil",
|
||||
"Thai": "Thais",
|
||||
|
|
|
|||
2
dist/ba_data/data/languages/english.json
vendored
2
dist/ba_data/data/languages/english.json
vendored
|
|
@ -1632,6 +1632,7 @@
|
|||
"Indonesian": null,
|
||||
"Italian": null,
|
||||
"Japanese": null,
|
||||
"Kazakh": null,
|
||||
"Korean": null,
|
||||
"Malay": null,
|
||||
"Persian": null,
|
||||
|
|
@ -1763,6 +1764,7 @@
|
|||
"You got an achievement reward!": null,
|
||||
"You have been promoted to a new league; congratulations!": 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 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,
|
||||
|
|
|
|||
36
dist/ba_data/data/languages/filipino.json
vendored
36
dist/ba_data/data/languages/filipino.json
vendored
|
|
@ -902,7 +902,7 @@
|
|||
"powerupPunchDescriptionText": "Ang iyong mga suntok ay mas mahirap,\nmas mabilis, mas mahusay, at mas malakas.",
|
||||
"powerupPunchNameText": "Guwantes",
|
||||
"powerupShieldDescriptionText": "Pumigil na pagsakit\nPara mas guminhawa.",
|
||||
"powerupShieldNameText": "Enrhiyang-Kalasag",
|
||||
"powerupShieldNameText": "Enerhiyang-Kalasag",
|
||||
"powerupStickyBombsDescriptionText": "Dumikit sa anumang matamaan nila.\nIto’y naging pagtawanan.",
|
||||
"powerupStickyBombsNameText": "Bombang-Malagkit",
|
||||
"powerupsSubtitleText": "Siyempre, 'di kumpleto ang laro kapag walang mga powerups:",
|
||||
|
|
@ -928,7 +928,7 @@
|
|||
"internal": {
|
||||
"arrowsToExitListText": "pindutin ang ${LEFT} o ${RIGHT} upang mawala sa listahan",
|
||||
"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.",
|
||||
"connectedToGameText": "Sumali sa '${NAME}'",
|
||||
"connectedToPartyText": "Sumali sa party ni ${NAME}!",
|
||||
|
|
@ -981,7 +981,7 @@
|
|||
"unableToCompleteTryAgainText": "Hindi maaaring maitapos ito sa ngayon.\nMaaaring mo ulitin.",
|
||||
"unableToResolveHostText": "Error: hindi malutas ang host.",
|
||||
"unavailableNoConnectionText": "Ito’y 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.",
|
||||
"willTimeOutText": "(magta-time out kung idle)"
|
||||
},
|
||||
|
|
@ -993,16 +993,16 @@
|
|||
"keyboardChangeInstructionsText": "I-double press space para mapalitan ang mga keyboard.",
|
||||
"keyboardNoOthersAvailableText": "Walang ibang mga keyboard na magagamit.",
|
||||
"keyboardSwitchText": "Nagpapalit ng keyboard sa \"${NAME}\".",
|
||||
"kickOccurredText": "Napalayas si ${NAME} dito.",
|
||||
"kickQuestionText": "Palayasin si ${NAME}?",
|
||||
"kickText": "Palayasin",
|
||||
"kickOccurredText": "Natalsik na si ${NAME} dito.",
|
||||
"kickQuestionText": "Talsikin si ${NAME}?",
|
||||
"kickText": "Patalsikin",
|
||||
"kickVoteCantKickAdminsText": "Hindi pwedeng palayasin ang Mga Ninuno dito.",
|
||||
"kickVoteCantKickSelfText": "Hindi mo mailayas ang iyong sarili.",
|
||||
"kickVoteFailedNotEnoughVotersText": "Konti ang mga naglalaro upang magbutuhan.",
|
||||
"kickVoteFailedText": "Hindi maipalayas mula sa pagbutuhan.",
|
||||
"kickVoteStartedText": "Sinimulan ang butuhan upang palayasin dito si ${NAME}.",
|
||||
"kickVoteText": "Bumoto para Ipalayas",
|
||||
"kickVotingDisabledText": "Naka-disable ang kick voting.",
|
||||
"kickVoteStartedText": "Sinimulan ang butuhan upang patalsikin dito si ${NAME}.",
|
||||
"kickVoteText": "Bumoto para Patalsikin",
|
||||
"kickVotingDisabledText": "Nakasara ang boto sa pagtalsikin.",
|
||||
"kickWithChatText": "Pindutin ang ${YES} sa iyong puwang salitahan kung oo at ${NO} kung hindi.",
|
||||
"killsTallyText": "${COUNT} pinatay",
|
||||
"killsText": "Pinatay",
|
||||
|
|
@ -1177,7 +1177,7 @@
|
|||
"titleText": "Karakter ng 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",
|
||||
"playlistText": "Playlist",
|
||||
"playlistsText": "Mga Playlist",
|
||||
|
|
@ -1312,7 +1312,7 @@
|
|||
},
|
||||
"settingsWindowAdvanced": {
|
||||
"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",
|
||||
"devToolsText": "Kagamitan sa Paggawa",
|
||||
"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!",
|
||||
"insecureConnectionsDescriptionText": "Hindi ito irerekomenda, ngunit pwede maglaro nang online \nmula sa ibang bansa o network na limitado.",
|
||||
"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)",
|
||||
"languageText": "Wika",
|
||||
"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)",
|
||||
"iconsText": "Mga Tatak",
|
||||
"loadErrorText": "Hindi ma-load ang page.\nSuriin ang iyong koneksyon sa internet.",
|
||||
"loadingText": "Saglit lang…",
|
||||
"loadingText": "nagloload…",
|
||||
"mapsText": "Mga Mapa",
|
||||
"miniGamesText": "Mga Lalaruhin",
|
||||
"oneTimeOnlyText": "(isang beses lang)",
|
||||
|
|
@ -1642,6 +1642,7 @@
|
|||
"Indonesian": "Wikang Indonesiyo",
|
||||
"Italian": "Wikang Italiyano",
|
||||
"Japanese": "Wikang Hapon",
|
||||
"Kazakh": "Wikang Kazakh",
|
||||
"Korean": "Wikang Koreano",
|
||||
"Malay": "Wikang Malay",
|
||||
"Persian": "Wikang Persyano",
|
||||
|
|
@ -1754,7 +1755,7 @@
|
|||
"Temporarily unavailable; please try again later.": "Pansamantalang hindi magagamit; Subukang muli mamaya.",
|
||||
"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 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 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.",
|
||||
|
|
@ -1771,10 +1772,11 @@
|
|||
"You got ${COUNT} tickets!": "Nakakuha ka ng ${COUNT} tickets!",
|
||||
"You got ${COUNT} tokens!": "May nakuha ka na ${COUNT} tokens!",
|
||||
"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 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 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 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.",
|
||||
|
|
@ -2003,11 +2005,11 @@
|
|||
},
|
||||
"winsPlayerText": "Nanalo si ${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.",
|
||||
"workspaceSyncReuseText": "Hindi ma-sync ang ${WORKSPACE}. Muling paggamit ng nakaraang naka-sync na bersyon.",
|
||||
"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",
|
||||
"xbox360ControllersWindow": {
|
||||
"getDriverText": "Kunin ang Driver",
|
||||
|
|
|
|||
8
dist/ba_data/data/languages/french.json
vendored
8
dist/ba_data/data/languages/french.json
vendored
|
|
@ -1715,7 +1715,8 @@
|
|||
"Arabic": "Arabe",
|
||||
"Belarussian": "Biélorusse",
|
||||
"Chinese": "Chinois simplifié",
|
||||
"ChineseTraditional": "Chinois Traditionnel",
|
||||
"ChineseSimplified": "Chinois - Simplifié",
|
||||
"ChineseTraditional": "Chinois - Traditionnel",
|
||||
"Croatian": "Croate",
|
||||
"Czech": "Tchèque",
|
||||
"Danish": "Danois",
|
||||
|
|
@ -1733,17 +1734,22 @@
|
|||
"Indonesian": "Indonésien",
|
||||
"Italian": "Italien",
|
||||
"Japanese": "Japonais",
|
||||
"Kazakh": "Kazakh",
|
||||
"Korean": "Coréen",
|
||||
"Malay": "Malais",
|
||||
"Persian": "Persan",
|
||||
"PirateSpeak": "Parole de pirate",
|
||||
"Polish": "Polonais",
|
||||
"Portuguese": "Portugais",
|
||||
"PortugueseBrazil": "Portugais - Brésil",
|
||||
"PortuguesePortugal": "Portugais - Portugal",
|
||||
"Romanian": "Roumain",
|
||||
"Russian": "Russe",
|
||||
"Serbian": "Serbe",
|
||||
"Slovak": "Slovaque",
|
||||
"Spanish": "Espagnol",
|
||||
"SpanishLatinAmerica": "Espagnol - Amérique Latine",
|
||||
"SpanishSpain": "Espagnol - Espagne",
|
||||
"Swedish": "Suédois",
|
||||
"Tamil": "Tamil",
|
||||
"Thai": "Thaïlandais",
|
||||
|
|
|
|||
8
dist/ba_data/data/languages/german.json
vendored
8
dist/ba_data/data/languages/german.json
vendored
|
|
@ -1738,7 +1738,8 @@
|
|||
"Arabic": "Arabisch",
|
||||
"Belarussian": "Weißrussland",
|
||||
"Chinese": "Chinesisch vereinfacht",
|
||||
"ChineseTraditional": "Chinesisch Traditionell",
|
||||
"ChineseSimplified": "Chinesisch - Vereinfacht",
|
||||
"ChineseTraditional": "Chinesisch - Traditionell",
|
||||
"Croatian": "Kroatisch",
|
||||
"Czech": "Tschechisch",
|
||||
"Danish": "Dänisch",
|
||||
|
|
@ -1756,17 +1757,22 @@
|
|||
"Indonesian": "Indonesisch",
|
||||
"Italian": "Italienisch",
|
||||
"Japanese": "Japanisch",
|
||||
"Kazakh": "Kasachisch",
|
||||
"Korean": "Koreanisch",
|
||||
"Malay": "Malaiisch",
|
||||
"Persian": "Persisch",
|
||||
"PirateSpeak": "Piratensprache",
|
||||
"Polish": "Polnisch",
|
||||
"Portuguese": "Portugiesisch",
|
||||
"PortugueseBrazil": "Portugiesisch - Brasilien",
|
||||
"PortuguesePortugal": "Portugiesisch - Portugal",
|
||||
"Romanian": "Rumänisch",
|
||||
"Russian": "Russisch",
|
||||
"Serbian": "Serbisch",
|
||||
"Slovak": "Slovakisch",
|
||||
"Spanish": "Spanisch",
|
||||
"SpanishLatinAmerica": "Spanisch - Lateinamerika",
|
||||
"SpanishSpain": "Spanisch - Spanien",
|
||||
"Swedish": "Schwedisch",
|
||||
"Tamil": "Tamil",
|
||||
"Thai": "Thailändisch",
|
||||
|
|
|
|||
2
dist/ba_data/data/languages/gibberish.json
vendored
2
dist/ba_data/data/languages/gibberish.json
vendored
|
|
@ -1787,6 +1787,7 @@
|
|||
"Indonesian": "Inofiqdson",
|
||||
"Italian": "Itzllfjssnn",
|
||||
"Japanese": "Capnokas",
|
||||
"Kazakh": "Kfwefz",
|
||||
"Korean": "Kornesnzn",
|
||||
"Malay": "FJwoerjjdf",
|
||||
"Persian": "Psdfsdf",
|
||||
|
|
@ -1924,6 +1925,7 @@
|
|||
"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 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 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.",
|
||||
|
|
|
|||
7
dist/ba_data/data/languages/hindi.json
vendored
7
dist/ba_data/data/languages/hindi.json
vendored
|
|
@ -1628,7 +1628,8 @@
|
|||
"Arabic": "अरबी",
|
||||
"Belarussian": "बेलारूसी",
|
||||
"Chinese": "सरलीकृत चीनी",
|
||||
"ChineseTraditional": "चीनी पारंपरिक",
|
||||
"ChineseSimplified": "चीनी - सरलीकृत",
|
||||
"ChineseTraditional": "चीनी - परंपरागत",
|
||||
"Croatian": "क्रोएशियाई",
|
||||
"Czech": "चेक",
|
||||
"Danish": "डेनिश",
|
||||
|
|
@ -1652,11 +1653,15 @@
|
|||
"PirateSpeak": "डाकू भाषा",
|
||||
"Polish": "पोलिश",
|
||||
"Portuguese": "पुर्तगाली",
|
||||
"PortugueseBrazil": "पुर्तगाली - ब्राज़ील",
|
||||
"PortuguesePortugal": "पुर्तगाली - पुर्तगाल",
|
||||
"Romanian": "रोमानियाई",
|
||||
"Russian": "रूसी",
|
||||
"Serbian": "सर्बियाई",
|
||||
"Slovak": "स्लोवाक",
|
||||
"Spanish": "स्पेनिश",
|
||||
"SpanishLatinAmerica": "स्पेनिश - लैटिन अमेरिका",
|
||||
"SpanishSpain": "स्पेनिश - स्पेन",
|
||||
"Swedish": "स्वीडिश",
|
||||
"Tamil": "तामिल",
|
||||
"Thai": "थाई",
|
||||
|
|
|
|||
38
dist/ba_data/data/languages/indonesian.json
vendored
38
dist/ba_data/data/languages/indonesian.json
vendored
|
|
@ -1628,7 +1628,8 @@
|
|||
"Arabic": "Arab",
|
||||
"Belarussian": "Belarusia",
|
||||
"Chinese": "Mandarin (disederhanakan) ",
|
||||
"ChineseTraditional": "Mandarin Tradisional",
|
||||
"ChineseSimplified": "Mandarin - Disederhanakan",
|
||||
"ChineseTraditional": "Mandarin - Tradisional",
|
||||
"Croatian": "Kroasia",
|
||||
"Czech": "Ceko",
|
||||
"Danish": "Denmark",
|
||||
|
|
@ -1646,17 +1647,22 @@
|
|||
"Indonesian": "Bahasa Indonesia",
|
||||
"Italian": "Italia",
|
||||
"Japanese": "Jepang",
|
||||
"Kazakh": "Kazakh",
|
||||
"Korean": "Korea",
|
||||
"Malay": "Bahasa Malaysia",
|
||||
"Persian": "Persia",
|
||||
"PirateSpeak": "Omongan Bajak Laut",
|
||||
"PirateSpeak": "Logat Bajak Laut",
|
||||
"Polish": "Polandia",
|
||||
"Portuguese": "Portugis",
|
||||
"PortugueseBrazil": "Portugis - Brazil",
|
||||
"PortuguesePortugal": "Portugis - Portugal",
|
||||
"Romanian": "Romania",
|
||||
"Russian": "Rusia",
|
||||
"Serbian": "Serbia",
|
||||
"Slovak": "Slovakia",
|
||||
"Spanish": "Spanyol",
|
||||
"SpanishLatinAmerica": "Spanyol - Amerika Latin",
|
||||
"SpanishSpain": "Spanyol",
|
||||
"Swedish": "Swedia",
|
||||
"Tamil": "Tamil",
|
||||
"Thai": "Thai",
|
||||
|
|
@ -1819,8 +1825,8 @@
|
|||
"None": "Tak Satupun",
|
||||
"Normal": "Normal",
|
||||
"Pro Mode": "Mode Ahli",
|
||||
"Respawn Times": "Muncul Kembali Hingga",
|
||||
"Score to Win": "Skor Menang",
|
||||
"Respawn Times": "Hidup Kembali Hingga",
|
||||
"Score to Win": "Skor 'tuk Menang",
|
||||
"Short": "Pendek",
|
||||
"Shorter": "Pendek Sekali",
|
||||
"Solo Mode": "Mode Tunggal",
|
||||
|
|
@ -1926,7 +1932,7 @@
|
|||
"randomName4Text": "Udin",
|
||||
"randomName5Text": "Agus",
|
||||
"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...",
|
||||
"toSkipPressAnythingText": "(tekan apa saja untuk melompati pembelajaran)"
|
||||
},
|
||||
|
|
@ -1935,7 +1941,7 @@
|
|||
"unavailableText": "tidak tersedia",
|
||||
"unclaimedPrizesText": "Kamu punya hadiah yang belum diterima!",
|
||||
"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:",
|
||||
"unlockThisText": "Untuk buka ini, kamu membutuhkan:",
|
||||
"unsupportedControllerText": "Maaf, pengontrol \"${NAME}\" tidak didukung.",
|
||||
|
|
@ -1953,7 +1959,7 @@
|
|||
"usingItunesTurnRepeatAndShuffleOnText": "Tolong pastikan lagu diacak dan diulang di iTunes. ",
|
||||
"v2AccountLinkingInfoText": "Untuk menautkan akun V2, gunakan tombol 'Manajemen Akun'.",
|
||||
"v2AccountRequiredText": "Membutuhkan akun V2. Upgrade akunmu dan coba lagi.",
|
||||
"validatingTestBuildText": "Memvalidasi Tes Build...",
|
||||
"validatingTestBuildText": "Memvalidasi Build Percobaan...",
|
||||
"viaText": "melalui",
|
||||
"victoryText": "Menang!",
|
||||
"voteDelayText": "Kamu tidak dapat memulai pemilihan suara dalam ${NUMBER} detik",
|
||||
|
|
@ -1968,19 +1974,19 @@
|
|||
"watchAnAdText": "Tonton Iklan",
|
||||
"watchWindow": {
|
||||
"deleteConfirmText": "Hapus \"${REPLAY}\"?",
|
||||
"deleteReplayButtonText": "Hapus\nReplay",
|
||||
"myReplaysText": "Replayku",
|
||||
"noReplaySelectedErrorText": "Tidak Ada Replay Terpilih",
|
||||
"deleteReplayButtonText": "Hapus\nRekaman",
|
||||
"myReplaysText": "Rekamanku",
|
||||
"noReplaySelectedErrorText": "Tidak Ada Rekaman Terpilih",
|
||||
"playbackSpeedText": "Kecepatan Pemutaran: ${SPEED}",
|
||||
"renameReplayButtonText": "Ganti Nama\nReplay",
|
||||
"renameReplayButtonText": "Ganti Nama\nRekaman",
|
||||
"renameReplayText": "Mengubah nama \"${REPLAY}\" menjadi:",
|
||||
"renameText": "Ganti Nama",
|
||||
"replayDeleteErrorText": "Kesalahan menghapus replay.",
|
||||
"replayDeleteErrorText": "Kesalahan menghapus rekaman.",
|
||||
"replayNameText": "Nama Rekaman",
|
||||
"replayRenameErrorAlreadyExistsText": "Rekaman dengan nama tersebut sudah ada",
|
||||
"replayRenameErrorInvalidName": "Tidak dapat mengganti nama rekaman; nama tidak valid",
|
||||
"replayRenameErrorText": "Error mengganti nama rekaman",
|
||||
"sharedReplaysText": "Replay Yang Dibagikan",
|
||||
"replayRenameErrorAlreadyExistsText": "Rekaman dengan nama tersebut sudah ada.",
|
||||
"replayRenameErrorInvalidName": "Tidak dapat mengganti nama rekaman; nama tidak valid.",
|
||||
"replayRenameErrorText": "Error mengganti nama rekaman.",
|
||||
"sharedReplaysText": "Rekaman Yang Dibagikan",
|
||||
"titleText": "Tonton",
|
||||
"watchReplayButtonText": "Lihat\nReplay"
|
||||
},
|
||||
|
|
|
|||
7
dist/ba_data/data/languages/italian.json
vendored
7
dist/ba_data/data/languages/italian.json
vendored
|
|
@ -1702,7 +1702,8 @@
|
|||
"Arabic": "Arabo",
|
||||
"Belarussian": "Bielorusso",
|
||||
"Chinese": "Cinese Semplificato",
|
||||
"ChineseTraditional": "Cinese Tradizionale",
|
||||
"ChineseSimplified": "Cinese - Semplificato",
|
||||
"ChineseTraditional": "Cinese - Tradizionale",
|
||||
"Croatian": "Croato",
|
||||
"Czech": "Ceco",
|
||||
"Danish": "Danese",
|
||||
|
|
@ -1726,11 +1727,15 @@
|
|||
"PirateSpeak": "Piratese",
|
||||
"Polish": "Polacco",
|
||||
"Portuguese": "Portoghese",
|
||||
"PortugueseBrazil": "Portoghese - Brasile",
|
||||
"PortuguesePortugal": "Portoghese - Portogallo",
|
||||
"Romanian": "Rumeno",
|
||||
"Russian": "Russo",
|
||||
"Serbian": "Serbo",
|
||||
"Slovak": "Slovacco",
|
||||
"Spanish": "Spagnolo",
|
||||
"SpanishLatinAmerica": "Spagnolo - America Latina",
|
||||
"SpanishSpain": "Spagnolo - Spagna",
|
||||
"Swedish": "Svedese",
|
||||
"Tamil": "Tamil",
|
||||
"Thai": "Tailandese",
|
||||
|
|
|
|||
1989
dist/ba_data/data/languages/kazakh.json
vendored
Normal file
1989
dist/ba_data/data/languages/kazakh.json
vendored
Normal file
File diff suppressed because it is too large
Load diff
14
dist/ba_data/data/languages/korean.json
vendored
14
dist/ba_data/data/languages/korean.json
vendored
|
|
@ -380,7 +380,8 @@
|
|||
"reduceWaitText": "대기 시간 단축",
|
||||
"slotDescriptionText": "이 슬롯은 상자를 보관할 수 있습니다.\n\n캠페인 레벨을 플레이하고,\n토너먼트에서 순위를 매기고,\n업적을 달성하여 상자를 획득하세요.",
|
||||
"slotText": "상자 슬롯 ${NUM}",
|
||||
"slotsFullWarningText": "경고: 모든 상자 슬롯이 찼습니다.\n이 게임에서 획득한 상자는 모두 사라지게 됩니다."
|
||||
"slotsFullWarningText": "경고: 모든 상자 슬롯이 찼습니다.\n이 게임에서 획득한 상자는 모두 사라지게 됩니다.",
|
||||
"unlocksInText": "잠금 해제"
|
||||
},
|
||||
"choosingPlayerText": "<플레이어 선택>",
|
||||
"claimText": "청구",
|
||||
|
|
@ -1380,6 +1381,7 @@
|
|||
},
|
||||
"spaceKeyText": "스페이스",
|
||||
"statsText": "전적",
|
||||
"stopRemindingMeText": "그만 표시하기",
|
||||
"storagePermissionAccessText": "이 행위는 저장소 접근이 필요합니다.",
|
||||
"store": {
|
||||
"alreadyOwnText": "이미 ${NAME}(을)를 소유 중입니다!",
|
||||
|
|
@ -1454,6 +1456,7 @@
|
|||
"getTokensText": "토큰 얻기",
|
||||
"notEnoughTokensText": "토큰이 부족합니다!",
|
||||
"numTokensText": "${COUNT}개 토큰",
|
||||
"openNowDescriptionText": "지금 상자를 열 수 있는\n토큰이 충분합니다.\n기다릴 필요가 없습니다.",
|
||||
"shinyNewCurrencyText": "BombSquad의 반짝이는 새로운 화폐.",
|
||||
"tokenPack1Text": "작은 토큰 팩",
|
||||
"tokenPack2Text": "중간 토큰 팩",
|
||||
|
|
@ -1617,7 +1620,8 @@
|
|||
"Arabic": "아랍어",
|
||||
"Belarussian": "벨로루시어",
|
||||
"Chinese": "중국어 간체",
|
||||
"ChineseTraditional": "중국어 번체",
|
||||
"ChineseSimplified": "중국어 간체자",
|
||||
"ChineseTraditional": "중국어 번체자",
|
||||
"Croatian": "크로아티아어",
|
||||
"Czech": "체코어",
|
||||
"Danish": "덴마크어",
|
||||
|
|
@ -1641,11 +1645,15 @@
|
|||
"PirateSpeak": "해적의 말",
|
||||
"Polish": "폴란드어",
|
||||
"Portuguese": "포르투갈어",
|
||||
"PortugueseBrazil": "브라질 포르투갈어",
|
||||
"PortuguesePortugal": "포르투갈어",
|
||||
"Romanian": "루마니아어",
|
||||
"Russian": "러시아어",
|
||||
"Serbian": "세르비아어",
|
||||
"Slovak": "슬로바키아어",
|
||||
"Spanish": "스페인어",
|
||||
"SpanishLatinAmerica": "라틴 아메리카 스페인어",
|
||||
"SpanishSpain": "스페인어",
|
||||
"Swedish": "스웨덴어",
|
||||
"Tamil": "타밀어",
|
||||
"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(일부 상황에서 도전과제 빼고는)",
|
||||
"WARNING: complaints of hacking have been issued against your account.\nAccounts found to be hacking will be banned. Please play fair.": "경고: 당신 계정에 해킹 관련 경고가 전해졌습니다.\n해킹 중인 걸로 밝혀진 계정은 즉시 차단됩니다. 제발 게임만은 공정하게 합시다.",
|
||||
"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경고: 이 작업은 취소할 수 없습니다!",
|
||||
"You already own this!": "이미 소유 중입니다!",
|
||||
"You can join in ${COUNT} seconds.": "${COUNT} 초 후에 참가할 수 있습니다.",
|
||||
|
|
@ -1921,6 +1930,7 @@
|
|||
"twoKillText": "더블 킬!",
|
||||
"uiScaleText": "Ui 크기",
|
||||
"unavailableText": "이용할 수 없음",
|
||||
"unclaimedPrizesText": "청구되지 않은 상품이 있습니다!",
|
||||
"unconfiguredControllerDetectedText": "구성되지 않은 컨트롤러가 검색됨:",
|
||||
"unlockThisInTheStoreText": "상점에서 잠금 해제해야 합니다.",
|
||||
"unlockThisProfilesText": "${NUM}개 이상의 프로필을 만들기 위해, 다음 사항이 필요합니다. :",
|
||||
|
|
|
|||
180
dist/ba_data/data/languages/persian.json
vendored
180
dist/ba_data/data/languages/persian.json
vendored
|
|
@ -166,10 +166,10 @@
|
|||
"name": "جادوگر ${LEVEL}"
|
||||
},
|
||||
"Precision Bombing": {
|
||||
"description": "بدون گرفتن هیچ قدرتی برنده شو",
|
||||
"descriptionComplete": "بدون گرفتن هیچ قدرتی برنده شدی",
|
||||
"descriptionFull": "${LEVEL} رو بدون گرفتن هیچ قدرتی برنده شو",
|
||||
"descriptionFullComplete": "${LEVEL} رو بدون گرفتن هیچ قدرتی برنده شدی",
|
||||
"description": "بدون گرفتن هیچ نیروزایی برنده شو",
|
||||
"descriptionComplete": "بدون گرفتن هیچ نیروزایی برنده شدی",
|
||||
"descriptionFull": "${LEVEL} رو بدون گرفتن هیچ نیروزایی برنده شو",
|
||||
"descriptionFullComplete": "${LEVEL} رو بدون گرفتن هیچ نیروزایی برنده شدی",
|
||||
"name": "بمبباران دقیق"
|
||||
},
|
||||
"Pro Boxer": {
|
||||
|
|
@ -184,7 +184,7 @@
|
|||
"descriptionComplete": "بدون اینکه بزاری حریف امتیاز بگیره، برنده شدی",
|
||||
"descriptionFull": "در ${LEVEL} بدون اینکه بزاری حریف امتیاز بگیره، برنده شو",
|
||||
"descriptionFullComplete": "در ${LEVEL} بدون اینکه بزاری حریف امتیاز بگیره، برنده شدی",
|
||||
"name": "دوازهبسته در ${LEVEL}"
|
||||
"name": "دروازهبسته در ${LEVEL}"
|
||||
},
|
||||
"Pro Football Victory": {
|
||||
"description": "برنده شو",
|
||||
|
|
@ -377,10 +377,10 @@
|
|||
"chests": {
|
||||
"prizeOddsText": "جایزه خفن",
|
||||
"reduceWaitText": "کاهش انتظار",
|
||||
"slotDescriptionText": "این مکان می تواند یک صندوق را نگه دارد\n\nبا بازی سطوح کمپین،\nقرار گرفتن در مسابقات و تکمیل\nدستاوردها صندوق بدست آورید",
|
||||
"slotText": "محل صندوق ${NUM}",
|
||||
"slotDescriptionText": "این شکاف میتواند یک صندوق نگه دارد.\n\nبا بازی در مرحلههای کمپین، رتبهگیری در\nناوردگان و تکمیل دستاوردها صندوق\nبدست آورید.",
|
||||
"slotText": "${NUM} شکاف صندوق",
|
||||
"slotsFullWarningText": "هشدار: تمام محل های صندوق شما پر است.\n هر صندوق ای که در این بازی به دست آورید از بین خواهد رفت",
|
||||
"unlocksInText": "باز می کند"
|
||||
"unlocksInText": "باز میشود در عرض"
|
||||
},
|
||||
"choosingPlayerText": "<انتخاب بازیکن>",
|
||||
"claimText": "دریافت",
|
||||
|
|
@ -396,11 +396,11 @@
|
|||
"ps3Text": "PS3 دسته",
|
||||
"titleText": "دستهها",
|
||||
"wiimotesText": "ها Wiimote",
|
||||
"xbox360Text": "Xbox 360 دسته"
|
||||
"xbox360Text": "Xbox 360 دستهی"
|
||||
},
|
||||
"configGamepadSelectWindow": {
|
||||
"androidNoteText": "تذکر: پشتیبانی از دسته با توجه به دستگاه و نسخه ی اندرویدش متفاوت است",
|
||||
"pressAnyButtonText": "یکی از دکمه های دسته ای که میخواهید\n تنظیم کنید را فشار دهید",
|
||||
"androidNoteText": "تذکر: پشتیبانی از دسته با توجه به دستگاه و نسخهی اندروید متفاوت است.",
|
||||
"pressAnyButtonText": "یکی از دکمههای دستهای که میخواهید\nتنظیم کنید را فشار دهید...",
|
||||
"titleText": "تنظیم دسته"
|
||||
},
|
||||
"configGamepadWindow": {
|
||||
|
|
@ -453,15 +453,15 @@
|
|||
"keyboard2NoteText": "تذکر:بیشتر کیبوردها فقط چند دکمه را همزمان میپذیرند\nپس داشتن یه کیبورد دیگه ممکنه مفید باشه\nاگر کیبورد متصل و جدای دیگه هم برای استفاده باشه\nتوجه کنید که هنوز هم لازمه دکمه های خاصی رو \nبرای دو کیبورد تنظیم کنید"
|
||||
},
|
||||
"configTouchscreenWindow": {
|
||||
"actionControlScaleText": "اندازه ی دکمه ها",
|
||||
"actionControlScaleText": "اندازهی دکمهها",
|
||||
"actionsText": "اعمال",
|
||||
"buttonsText": "کلید ها",
|
||||
"buttonsText": "دکمه",
|
||||
"dragControlsText": "< دکمهها را بکشید و موقعیتشان را تعیین کنید >",
|
||||
"joystickText": "دکمه ی حرکت",
|
||||
"movementControlScaleText": "اندازه ی دکمه ی حرکت",
|
||||
"joystickText": "اهرمک",
|
||||
"movementControlScaleText": "اندازهی دکمهی حرکت",
|
||||
"movementText": "حرکت",
|
||||
"resetText": "بازنشانی",
|
||||
"swipeControlsHiddenText": "مخفی کردن دکمه ی حرکت",
|
||||
"swipeControlsHiddenText": "پنهان کردن آیکون حرکت جاروبی",
|
||||
"swipeInfoText": "کمی طول میکشد به این نوع حرکت عادت کنید\nولی راحت باشید و بدون نگاه کردن به آن بازی کنید",
|
||||
"swipeText": "حرکت جاروبی",
|
||||
"titleText": "پیکربندی صفحه لمسی"
|
||||
|
|
@ -488,7 +488,7 @@
|
|||
"activenessAllTimeInfoText": "بر روی ردهبندی کلی اِعمال نمیشود.",
|
||||
"activenessInfoText": "این افزاینده در روزهایی که بازی میکنید افزایش مییابد\nو در روزهایی که بازی نمیکنید کاهش مییابد.",
|
||||
"activityText": "فعالیت",
|
||||
"campaignText": "پیشروی",
|
||||
"campaignText": "پیشروی",
|
||||
"challengesInfoText": "برای کامل کردن مینیبازیها جایزه بگیرید.\n\nهرگاه چالشی را انجام میدهید، جایزهها و\nسختی مراحل افزایش مییابد و هرگاه چالشی\nباطل شود یا به هدر رود، کاهش مییابد.",
|
||||
"challengesText": "چالشها",
|
||||
"currentBestText": "بهترین امتیاز کنونی",
|
||||
|
|
@ -517,9 +517,9 @@
|
|||
"timeRemainingText": "زمان باقیمانده",
|
||||
"toRankedText": "تا رتبهبندی شوید",
|
||||
"totalText": "مجموع",
|
||||
"tournamentInfoText": "بر سر امتیاز بیشتر با بازیکنان در\nلیگ خود رقابت کنید.\n\nهنگامی که زمان مسابقه تمام شود، جایزه به\nنفرات برتر با امتیازهای بالا داده میشود.",
|
||||
"welcome1Text": "خوش آمدید. شما میتوانید ${LEAGUE} به لیگ\nبا گرفتن امتیاز، کامل کردن دستاوردها یا گرفتن جام\n.در مسابقات رتبهٔ خود را بهبود بخشید",
|
||||
"welcome2Text": "همچنین میتوانید از راههای مشابه بلیت جمعآوری کنید.\nبلیتها میتوانند برای باز کردن بازیکنان جدید، نقشهها، مینیبازیها یا برای ورود در مسابقهها و موارد\nبیشتر مورد استفاده قرار گیرند.",
|
||||
"tournamentInfoText": "بر سر امتیاز بیشتر با بازیکنان در\nلیگ خود رقابت کنید.\n\nهنگامی که زمان ناورد تمام شود، جایزه به\nنفرات برتر با امتیازهای بالا داده میشود.",
|
||||
"welcome1Text": "خوش آمدید. شما میتوانید ${LEAGUE} به لیگ\nبا گرفتن امتیاز، کامل کردن دستاوردها یا گرفتن جام\n.در ناوردگان، رتبهٔ خود را بهبود بخشید",
|
||||
"welcome2Text": "همچنین میتوانید از راههای مشابه بلیت جمعآوری کنید.\nبلیتها میتوانند برای بازگشایی شخصیتها، نقشهها و مینیبازیهای\nجدید یا برای ورود به ناوردگان و موارد بیشتر، بهکار میروند.",
|
||||
"yourPowerRankingText": "رتبهبندی قدرت شما:"
|
||||
},
|
||||
"copyConfirmText": "در حافظه کلیپ بورد شما کپی شد.",
|
||||
|
|
@ -630,27 +630,27 @@
|
|||
"cantEditDefaultText": ".نمیتوانید صدای پیشفرض رو دست کاری کنید. آن را کپی کنید یا یه جدید بسازید",
|
||||
"cantOverwriteDefaultText": "نمیشه صدای پیشفرض رو بازنویسی کرد",
|
||||
"cantSaveAlreadyExistsText": "یک صدا با همین نام وجود داره",
|
||||
"defaultGameMusicText": "<موسیقی پیش فرض بازی>",
|
||||
"defaultSoundtrackNameText": "صدای پیش فرض",
|
||||
"defaultGameMusicText": "<موسیقی پیش فرض بازی>",
|
||||
"defaultSoundtrackNameText": "صدای پیشفرض",
|
||||
"deleteConfirmText": "حذف صدا با نام:\n\n'${NAME}'?",
|
||||
"deleteText": "حذف\nصدا",
|
||||
"duplicateText": "ایجاد کپی\nاز صدا",
|
||||
"editSoundtrackText": "ویرایشگر صدا",
|
||||
"editText": "ویرایش\nصدا",
|
||||
"fetchingITunesText": "گرفتن صدا از لیست پخش برنامه",
|
||||
"musicVolumeZeroWarning": "هشدار:درجه ی صدای موسیقی روی صفر است",
|
||||
"musicVolumeZeroWarning": "هشدار: درجه صدای موسیقی روی 0 است",
|
||||
"nameText": "نام",
|
||||
"newSoundtrackNameText": "${COUNT} صدای من",
|
||||
"newSoundtrackText": "صدای جدید:",
|
||||
"newText": "صدای\nجدید",
|
||||
"selectAPlaylistText": "انتخاب یه لیست",
|
||||
"selectASourceText": "منبع موسیقی",
|
||||
"testText": "آزمایشی",
|
||||
"titleText": "صداهای پس زمینه",
|
||||
"testText": "آزمایش",
|
||||
"titleText": "موسیقیمتنها",
|
||||
"useDefaultGameMusicText": "موسیقی پیشفرض بازی",
|
||||
"useITunesPlaylistText": "لیست موسیقی برنامه",
|
||||
"useMusicFileText": "(...و mp3)فایل موسیقی",
|
||||
"useMusicFolderText": "پوشه ی فایل های موسیقی"
|
||||
"useMusicFileText": "(...و mp3) فایل موسیقی",
|
||||
"useMusicFolderText": "پوشهی فایلهای موسیقی"
|
||||
},
|
||||
"editText": "ویرایش",
|
||||
"enabledText": "فعال",
|
||||
|
|
@ -713,7 +713,7 @@
|
|||
"gamesToText": "${LOSECOUNT} بازی به ${WINCOUNT}",
|
||||
"gatherWindow": {
|
||||
"aboutDescriptionLocalMultiplayerExtraText": "فراموش نکنید: هردستگاه در یک گروه میتواند بیشتر\n.از یک بازیکن داشته باشد اگر به اندازه ی کافی دسته دارید",
|
||||
"aboutDescriptionText": ".از این زبانهها برای تشکیل یک پارتی استفاده کنید\n\nپارتی به شما این امکان را میدهد که بازیها و مسابقات\n.را با دوستانتان بر روی گوشیهای متفاوت بازی کنید\n\nدر گوشهی بالای سمت راست استفاده کنید ${PARTY} از دکمهی\n.تا با پارتی چت و تعامل کنید\n(را هنگامی که در منو هستید فشار دهید ${BUTTON} با دسته، دکمهی)",
|
||||
"aboutDescriptionText": ".از این زبانهها برای تشکیل یک پارتی استفاده کنید\n\nپارتی به شما این امکان را میدهد که بازیها و ناوردگان\n.را با دوستانتان بر روی گوشیهای متفاوت بازی کنید\n\nدر گوشهی بالای سمت راست استفاده کنید ${PARTY} از دکمهی\n.تا با پارتی چت و تعامل کنید\n(را هنگامی که در منو هستید فشار دهید ${BUTTON} با دسته، دکمهی)",
|
||||
"aboutText": "درباره",
|
||||
"addressFetchErrorText": "<خطا در اتصال به آدرس>",
|
||||
"appInviteMessageText": "${APP_NAME}بلیت فرستاده در برنامه ی ${COUNT}برای شما ${NAME}",
|
||||
|
|
@ -786,7 +786,7 @@
|
|||
"partyInviteText": "شما را دعوت کرده${NAME} \nتا به گروهشان ملحق شوید",
|
||||
"partyNameText": "نام پارتی",
|
||||
"partyServerRunningText": ".سرور پارتی شما در حال اجراست",
|
||||
"partySizeText": "اندازه دسته",
|
||||
"partySizeText": "اندازهی پارتی",
|
||||
"partyStatusCheckingText": "در حال چک کردن وضعیت...",
|
||||
"partyStatusJoinableText": "گروه شما حالا دیگه از طریق اینترنت قابل اتصال برای بقیه است.",
|
||||
"partyStatusNoConnectionText": "عدم توانایی برقراری ارتباط با سرور",
|
||||
|
|
@ -794,7 +794,7 @@
|
|||
"partyStatusNotPublicText": "پارتی شما عمومی نیست",
|
||||
"pingText": "پینگ",
|
||||
"portText": "درگاه",
|
||||
"privatePartyCloudDescriptionText": ".گروههای خصوصی بر روی سرورهای ابری اختصاصی اجرا می شوند; و نیازی به پیکربندی روتر/مودم نیست",
|
||||
"privatePartyCloudDescriptionText": ".پارتیهای خصوصی روی سرورهای ابری اختصاصی اجرا میشوند؛ نیازی به پیکربندی روتر/مودم نیست",
|
||||
"privatePartyHostText": "میزبانی پارتی خصوصی",
|
||||
"privatePartyJoinText": "پیوستن به سرور خصوصی",
|
||||
"privateText": "خصوصی",
|
||||
|
|
@ -835,7 +835,7 @@
|
|||
"titleText": "بلیط بگیرید",
|
||||
"unavailableLinkAccountText": ".ببخشید،خرید به وسیله ی این دستگاه در دسترس نمیباشد\nمیتوانید حسابتان بر روی این دستگاه را به حسابی در دستگاهی\n.دیگر متصل کنید و آنجا خرید خود را انجام دهید",
|
||||
"unavailableTemporarilyText": "در حال حاضر در دسترس نمیباشد. لطفا بعدا دوباره امتحان کنید",
|
||||
"unavailableText": "متاسفانه , دردسترس نیست",
|
||||
"unavailableText": "متاسفیم، این مورد در دسترس نیست.",
|
||||
"versionTooOldText": "متاسفم،این ورژن بازی خیلی قدیمی است. لطفا بازی را آپدیت کنید",
|
||||
"youHaveShortText": "دارید ${COUNT} شما",
|
||||
"youHaveText": ".بلیط دارید ${COUNT} شما"
|
||||
|
|
@ -952,8 +952,8 @@
|
|||
"incompatibleVersionHostText": "میزبان در حال اجرا از نسخه دیگری از این بازی است.\nمطمئن شوید که شما هر دو تا بروز هستید و دوباره امتحان کنید.",
|
||||
"incompatibleVersionPlayerText": "${NAME} در حال اجرا از نسخه دیگری از این بازی است.\nمطمئن شوید که شما هر دو تا بروز هستید و دوباره امتحان کنید.",
|
||||
"invalidAddressErrorText": "خطا: آدرس در دسترس نیست",
|
||||
"invalidNameErrorText": "نام نا معتبر",
|
||||
"invalidPortErrorText": "مشکل:درگاه بی اعتبار",
|
||||
"invalidNameErrorText": "خطا: نام نامعتبر.",
|
||||
"invalidPortErrorText": "خطا: درگاه نامعتبر",
|
||||
"invitationSentText": "دعوت نامه ارسال شد.",
|
||||
"invitationsSentText": "دعوت نامه ارسال شد ${COUNT}",
|
||||
"joinedPartyInstructionsText": "فردی به گروه شما پیوسته\nبرید به بازی و بازی را شروع کنید",
|
||||
|
|
@ -967,7 +967,7 @@
|
|||
"playerJoinedPartyText": "به گروه بازی پیوست ${NAME}",
|
||||
"playerLeftPartyText": ".پارتی رو ترک کرد ${NAME}",
|
||||
"rejectingInviteAlreadyInPartyText": "رد کردن دعوت (already in a party)",
|
||||
"serverRestartingText": "سرور در حال شروع مجدد است. لطفا چند لحظه دیگر مجددا متصل شوید",
|
||||
"serverRestartingText": "سرور در حال بازراهاندازی است. لطفا در لحظهای دیگر دوباره بپیوندید...",
|
||||
"serverShuttingDownText": ".سرور درحال بسته شدن است",
|
||||
"signInErrorText": "خطای ورود به سیستم",
|
||||
"signInNoConnectionText": "ورود ناموفق بود. اتصال به اینترنت برقراره؟",
|
||||
|
|
@ -1023,14 +1023,14 @@
|
|||
"leagueRankText": "رتبه لیگ",
|
||||
"leagueText": "لیگ",
|
||||
"rankInLeagueText": "#${RANK}, ${NAME} League${SUFFIX}",
|
||||
"seasonEndedDaysAgoText": ".روز پیش پایان یافت ${NUMBER} فصل",
|
||||
"seasonEndsDaysText": ".روز دیگر پایان مییابد ${NUMBER} فصل",
|
||||
"seasonEndsHoursText": ".ساعت دیگر پایان مییابد ${NUMBER} فصل",
|
||||
"seasonEndsMinutesText": ".دقیقهی دیگر پایان مییابد ${NUMBER} فصل",
|
||||
"seasonEndedDaysAgoText": ".روز پیش پایان یافت ${NUMBER} فصل،",
|
||||
"seasonEndsDaysText": ".روز دیگر پایان مییابد ${NUMBER} فصل،",
|
||||
"seasonEndsHoursText": ".ساعت دیگر پایان مییابد ${NUMBER} فصل،",
|
||||
"seasonEndsMinutesText": ".دقیقهی دیگر پایان مییابد ${NUMBER} فصل،",
|
||||
"seasonText": "${NUMBER} فصل",
|
||||
"tournamentLeagueText": ".برسید ${NAME} برای ورود به این مسابقه، باید به لیگ",
|
||||
"trophyCountsResetText": ".جوایز در فصل بعد بازنشانی میشوند",
|
||||
"upToDateBonusDescriptionText": "بازیکنانی نسخه اخیر را اجرا می کنند بازی در اینجا جایزه دریافت می کند ${PERCENT}%",
|
||||
"tournamentLeagueText": "برای ورود به این ناورد، باید به لیگ ${NAME} برسید.",
|
||||
"trophyCountsResetText": ".جایزهها در فصل بعد بازنشانی میشوند",
|
||||
"upToDateBonusDescriptionText": "بازیکنانی که نسخهی اخیری از بازی را اجرا میکنند،\n.امتیاز اضافی در اینجا دریافت میکنند ${PERCENT}%",
|
||||
"upToDateBonusText": "پاداش بهروز بودن"
|
||||
},
|
||||
"learnMoreText": "بیشتر بدانید",
|
||||
|
|
@ -1107,11 +1107,11 @@
|
|||
"noExternalStorageErrorText": "محل ذخیره سازی در این دستگاه یافت نشد",
|
||||
"noGameCircleText": "GameCircleخطا: وارد نشدید به",
|
||||
"noMessagesText": "هیچ پیامی فعلا نیست",
|
||||
"noPluginsInstalledText": "متاسفانه افزونه ها نصب نشده",
|
||||
"noPluginsInstalledText": "افزونهای نصب نیست",
|
||||
"noScoresYetText": "هیچ امتیازی نیست",
|
||||
"noServersFoundText": "سروری یافت نشد.",
|
||||
"noThanksText": "نه مرسی",
|
||||
"noTournamentsInTestBuildText": ".هشدار: امتیازات مسابقه از این نسخهٔ آزمایشی نادیده گرفته میشوند",
|
||||
"noTournamentsInTestBuildText": ".هشدار: امتیازهای ناورد از این نسخهٔ آزمایشی نادیده گرفته میشوند",
|
||||
"noValidMapsErrorText": "هیچ نقشه معتبری برای این نوع بازی یافت نشد.",
|
||||
"notEnoughPlayersRemainingText": "بازیکنان باقیمانده کافی نیستند خارج بشید و دوباره یه بازی جدید رو شروع کنید",
|
||||
"notEnoughPlayersText": "!بازیکن نیاز دارید ${COUNT} برای شروع بازی حداقل به",
|
||||
|
|
@ -1153,13 +1153,13 @@
|
|||
"singlePlayerCoopText": "بازی تکنفره / چندنفره",
|
||||
"teamsText": "بازی تیمی"
|
||||
},
|
||||
"playText": "شروع بازی",
|
||||
"playText": "بازی",
|
||||
"playWindow": {
|
||||
"oneToFourPlayersText": "۱ تا ۴ بازیکن",
|
||||
"titleText": "شروع بازی",
|
||||
"twoToEightPlayersText": "۲ تا ۸ بازیکن"
|
||||
"oneToFourPlayersText": "۴-۱ بازیکن",
|
||||
"titleText": "بازی",
|
||||
"twoToEightPlayersText": "۸-۲ بازیکن"
|
||||
},
|
||||
"playerCountAbbreviatedText": "(نفره ${COUNT})",
|
||||
"playerCountAbbreviatedText": "${COUNT}p",
|
||||
"playerDelayedJoinText": ".در دور بعد وارد میشود ${PLAYER}",
|
||||
"playerInfoText": "اطلاعات بازیکن",
|
||||
"playerLeftText": ".بازی را ترک کرد ${PLAYER}",
|
||||
|
|
@ -1178,7 +1178,7 @@
|
|||
"playlistNotFoundText": "لیست بازی یافت نشد",
|
||||
"playlistText": "لیست بازی",
|
||||
"playlistsText": "لیست بازیها",
|
||||
"pleaseRateText": "اگر از ${APP_NAME} خوشتان آمده، لطفاً چند لحظهای وقت بگذارید و\nآن را رتبهبندی کنید یا مروری بر آن بنویسید. این کار بازخوردهای مفیدی\n.را به همراه دارد و به پشتیبانی از توسعهها در آینده کمک خواهد کرد\n\n!با تشکر\nاریک—",
|
||||
"pleaseRateText": "اگر از ${APP_NAME} خوشتان آمده، لطفاً چند لحظهای وقت بگذارید و\nآن را رتبهبندی کنید یا مروری بر آن بنویسید. این کار بازخوردهای مفیدی\n.را بههمراه دارد و به پشتیبانی از توسعهها در آینده کمک خواهد کرد\n\n!با تشکر\nاریک—",
|
||||
"pleaseWaitText": "…لطفاً صبر کنید",
|
||||
"pluginClassLoadErrorText": "${ERROR} :«${PLUGIN}» خطا در بارگیری دستهبندی افزونهٔ",
|
||||
"pluginInitErrorText": "${ERROR} :«${PLUGIN}» خطا در راهاندازی افزونهٔ",
|
||||
|
|
@ -1187,7 +1187,7 @@
|
|||
"pluginsDetectedText": "افزونه(ها)ی جدید شناسایی شد. آنها را در تنظیمات فعال، یا پیکربندی کنید.",
|
||||
"pluginsDisableAllText": "غیرفعال کردن همهی افزونهها",
|
||||
"pluginsEnableAllText": "فعال کردن همهی افزونهها",
|
||||
"pluginsRemovedText": "${NUM} افزونه دیگر یافت نمیشود.",
|
||||
"pluginsRemovedText": ".افزونه دیگر یافت نمیشود ${NUM}",
|
||||
"pluginsText": "افزونهها",
|
||||
"practiceText": "تمرین",
|
||||
"pressAnyButtonPlayAgainText": "فشردن هر دکمهای برای بازی دوباره…",
|
||||
|
|
@ -1260,7 +1260,7 @@
|
|||
"version_mismatch": "عدم تطابق نسخهها.\nاطمینان حاصل کنید که بمباسکواد و دستهمجازی\nآخرین نسخه باشند و دوباره امتحان کنید."
|
||||
},
|
||||
"removeInGameAdsText": "بازی را در فروشگاه بخرید تا تبلیغات حذف شوند «${PRO}» نسخهٔ",
|
||||
"removeInGameAdsTokenPurchaseText": "پیشنهاد زمان محدود: هر بسته توکن را برای حذف تبلیغات درون بازی خریداری کنید",
|
||||
"removeInGameAdsTokenPurchaseText": "پیشنهاد زمانمحدود: بسته توکنی را برای حذف تبلیغات درون بازی خریداری کنید",
|
||||
"renameText": "تغییر نام",
|
||||
"replayEndText": "پایان بازپخش",
|
||||
"replayNameDefaultText": "بازپخش بازی اخیر",
|
||||
|
|
@ -1281,7 +1281,7 @@
|
|||
"revertText": "بازگشت",
|
||||
"runText": "دویدن",
|
||||
"saveText": "ذخیره",
|
||||
"scanScriptsErrorText": "اسکریپت ها در حال بررسی خطا ها است؛ برای جزئیات لاگ را ببینید",
|
||||
"scanScriptsErrorText": "خطا هنگام اسکن اسکریپتها. برای جزییات لاگ را ببینید.",
|
||||
"scanScriptsMultipleModulesNeedUpdatesText": ".بهروزرسانی شوند api ${API} ماژول دیگر باید برای ${NUM} و ${PATH}",
|
||||
"scanScriptsSingleModuleNeedsUpdatesText": "${PATH} باید برای api ${API} بهروزرسانی شود.",
|
||||
"scoreChallengesText": "امتیاز چالش",
|
||||
|
|
@ -1309,7 +1309,7 @@
|
|||
"titleText": "تنظیمات"
|
||||
},
|
||||
"settingsWindowAdvanced": {
|
||||
"alwaysUseInternalKeyboardDescriptionText": "(یه کیبورد ساده و خوشدست برای نوشتن)",
|
||||
"alwaysUseInternalKeyboardDescriptionText": "(کیبورد ساده و خوشدست برای نوشتن)",
|
||||
"alwaysUseInternalKeyboardText": "همیشه از کیبورد داخلی استفاده شود",
|
||||
"benchmarksText": "معیار و تست استرس",
|
||||
"devToolsText": "ابزارهای توسعه",
|
||||
|
|
@ -1344,8 +1344,8 @@
|
|||
"translationFetchErrorText": "وضعیت ترجمه در دسترس نیست",
|
||||
"translationFetchingStatusText": "چک کردن وضعیت ترجمه ...",
|
||||
"translationInformMe": "!وقتی زبان من بهروزرسانی نیاز داشت، خبرم کن",
|
||||
"translationNoUpdateNeededText": "!زبان کنونی بهروز است. ایول به ولت",
|
||||
"translationUpdateNeededText": "!زبان کنونی نیاز به بهروزرسانی دارد",
|
||||
"translationNoUpdateNeededText": "!زبان بازی بهروز است. ایول به ولت",
|
||||
"translationUpdateNeededText": "** !!زبان بازی نیاز به بهروزرسانی دارد **",
|
||||
"vrTestingText": "VR تست"
|
||||
},
|
||||
"shareText": "اشتراکگذاری",
|
||||
|
|
@ -1399,7 +1399,7 @@
|
|||
"howToSwitchCharactersText": "(برو \"${SETTINGS} -> ${PLAYER_PROFILES}\" برای ایجاد و شخصیسازی شخصیتها به)",
|
||||
"howToUseIconsText": "((در بخش حساب کاربری) برای استفاده از اینها نمایههای جهانی ایجاد کنید)",
|
||||
"howToUseMapsText": "(از این نقشهها میتونید توی بازیهای تیمی و تکبهتک خودتون استفاده کنید)",
|
||||
"iconsText": "نشانهها",
|
||||
"iconsText": "نشانها",
|
||||
"loadErrorText": "لود شدن صفحه ناموفق بود\nاتصال اینترنت رو چک کنید",
|
||||
"loadingText": "در حال بارگزاری",
|
||||
"mapsText": "نقشهها",
|
||||
|
|
@ -1423,7 +1423,7 @@
|
|||
"storeDescriptionText": "8 بازیکن حزب جنون بازی!\n\nدوستان خود (یا رایانه) را در مسابقات مینی بازیهای انفجاری مانند: پرچم، هاکی و حرکت آهسته\n\nکنترل ساده و پشتیبانی گسترده میتوان تا حداکثر 8 نفر برای ورود به بازی اقدام کند؛ شما حتی میتوانید دستگاههای تلفنهمراه خود را به عنوان کنترل از طریق برنامه رایگان BombSquadremote استفاده کنید!\n\nبمباندازی از راه دور!\n\nwww.froemling.net/bombsquad را برای اطلاعات بیشتر چک کنید.",
|
||||
"storeDescriptions": {
|
||||
"blowUpYourFriendsText": "بازیکن دوستات رو بترکون 😁",
|
||||
"competeInMiniGamesText": "رقابت در بازیهای کوچک به یژه مسابقه پرواز.",
|
||||
"competeInMiniGamesText": "رقابت در بازیهای کوچک، از مسابقه تا پرواز.",
|
||||
"customize2Text": "سفارشی کردن شخصیتها، بازیهای کوچک، و حتی موسیقیهای متن .",
|
||||
"customizeText": "شما می تونید شخصیتها رو شخصیسازی کنید و به سلیقه خودتون لیستبازی درست کنید",
|
||||
"sportsMoreFunText": "ورزش ها سرگرم کننده تر میشن با انفجار",
|
||||
|
|
@ -1443,7 +1443,7 @@
|
|||
"testBuildValidatedText": "نسخه معتبر است؛ لذت ببرید.!",
|
||||
"thankYouText": "تشکر بخاطر حمایت از ما ! از بازی لذت ببرید",
|
||||
"threeKillText": "نابود کردن همزمان سه نفر",
|
||||
"ticketsDescriptionText": "بلیتها برای بازگشایی شخصیتها، نقشهها، بازیهای\nکوچک و موارد دیگر در فروشگاه به کار میروند. \n\nبلیتها در صندوقهایی که از کمپینها، مسابقات\nو دستاوردها برنده شدهاید، یافت میشوند.",
|
||||
"ticketsDescriptionText": "بلیتها برای بازگشایی شخصیتها، نقشهها، بازیهای\nکوچک و موارد دیگر در فروشگاه به کار میروند. \n\nبلیتها در صندوقهایی که از کمپینها، ناوردگان\nو دستاوردها برنده شدهاید، یافت میشوند.",
|
||||
"timeBonusText": "پاداش سرعت عمل",
|
||||
"timeElapsedText": "زمان گذشته",
|
||||
"timeExpiredText": "زمان تمام شده",
|
||||
|
|
@ -1458,7 +1458,7 @@
|
|||
"getTokensText": "دریافت توکن",
|
||||
"notEnoughTokensText": "توکن های شما کافی نیست !",
|
||||
"numTokensText": "توکن ${COUNT}",
|
||||
"openNowDescriptionText": "شما به اندازه کافی نشانه دارید\nاکنون این را باز کنید - اگر نکنید\nباید صبر کرد",
|
||||
"openNowDescriptionText": "شما بهاندازهی کافی توکن برای\nباز کردن آن دارید - نیازی نیست\nصبر کنید.",
|
||||
"shinyNewCurrencyText": "ارز های جدید بمب اسکواد",
|
||||
"tokenPack1Text": "بسته توکن کوچک",
|
||||
"tokenPack2Text": "بسته توکن متوسط",
|
||||
|
|
@ -1468,16 +1468,16 @@
|
|||
"youHaveGoldPassText": ".شما یک گلد پس دارید\n.تمامی خریدهای توکن رایگان است\n!لذت ببرید"
|
||||
},
|
||||
"topFriendsText": "بالاترین امتیاز دوستان",
|
||||
"tournamentCheckingStateText": "چک کردن وضعیت مسابقات؛ لطفا صبر کنید",
|
||||
"tournamentEndedText": "این دوره از مسابقات به پایان رسیده است دوره جدیدی بزودی آغاز خواهد شد",
|
||||
"tournamentEntryText": "ورودیِ مسابقات",
|
||||
"tournamentFinalStandingsText": "جدول رده بندی نهایی",
|
||||
"tournamentResultsRecentText": "آخرین نتایج مسابقات",
|
||||
"tournamentStandingsText": "جدول رده بندی مسابقات",
|
||||
"tournamentText": "جام حذفی",
|
||||
"tournamentTimeExpiredText": "زمان مسابقات پایان یافت",
|
||||
"tournamentsDisabledWorkspaceText": "وقتی فضاهای کاری فعال هستند، مسابقات غیرفعال می شوند.\n برای فعال کردن مجدد مسابقات، فضای کاری خود را غیرفعال کنید و دوباره راه اندازی کنید.",
|
||||
"tournamentsText": "مسابقات",
|
||||
"tournamentCheckingStateText": "چک کردن وضعیت ناوردگان؛ لطفن صبر کنید...",
|
||||
"tournamentEndedText": "این دوره از ناوردگان به پایان رسیدهاست. دورهی جدیدی بهزودی آغاز خواهد شد.",
|
||||
"tournamentEntryText": "ورودی ناوردگان",
|
||||
"tournamentFinalStandingsText": "جدول ردهبندی نهایی",
|
||||
"tournamentResultsRecentText": "نتایج ناوردگان اخیر",
|
||||
"tournamentStandingsText": "ردهبندی ناورد",
|
||||
"tournamentText": "ناورد",
|
||||
"tournamentTimeExpiredText": "زمان ناورد پایان یافت",
|
||||
"tournamentsDisabledWorkspaceText": "وقتی فضاهای کاری فعال هستند، ناوردگان غیرفعال میشود.\nبرای فعال کردن دوبارهی ناوردگان، فضای کاری خود را غیرفعال کنید و بازراهاندازی کنید.",
|
||||
"tournamentsText": "ناوردگان",
|
||||
"translations": {
|
||||
"characterNames": {
|
||||
"Agent Johnson": "مامور جانسون",
|
||||
|
|
@ -1513,8 +1513,8 @@
|
|||
"coopLevelNames": {
|
||||
"${GAME} Training": "تمرین ${GAME}",
|
||||
"Infinite ${GAME}": "${GAME} بیپایان",
|
||||
"Infinite Onslaught": "نبرد بیپایان",
|
||||
"Infinite Runaround": "دور بیپایان",
|
||||
"Infinite Onslaught": "نبرد بیپایان",
|
||||
"Infinite Runaround": "دور بیپایان",
|
||||
"Onslaught Training": "نبرد مبتدی",
|
||||
"Pro ${GAME}": "${GAME} حرفهای",
|
||||
"Pro Football": "فوتبال حرفهای",
|
||||
|
|
@ -1604,17 +1604,17 @@
|
|||
"Death Match": "نبرد مرگبار",
|
||||
"Easter Egg Hunt": "شکار ایستر اگ",
|
||||
"Elimination": "استقامت",
|
||||
"Football": "فوتبال آمریکایی",
|
||||
"Football": "فوتبال آمریکایی",
|
||||
"Hockey": "هاکی",
|
||||
"Keep Away": "دور نگه داشتن",
|
||||
"King of the Hill": "پادشاه تپه",
|
||||
"Meteor Shower": "بمبباران",
|
||||
"Ninja Fight": "نبرد با نینجاها",
|
||||
"Meteor Shower": "بمبباران",
|
||||
"Ninja Fight": "نبرد با نینجاها",
|
||||
"Onslaught": "مبارزه",
|
||||
"Race": "مسابقهی دو",
|
||||
"Race": "مسابقهی دو",
|
||||
"Runaround": "مانع",
|
||||
"Target Practice": "تمرین بمباندازی",
|
||||
"The Last Stand": "دفاع آخر"
|
||||
"Target Practice": "تمرین بمباندازی",
|
||||
"The Last Stand": "دفاع آخر"
|
||||
},
|
||||
"inputDeviceNames": {
|
||||
"Keyboard": "کیبورد",
|
||||
|
|
@ -1643,6 +1643,7 @@
|
|||
"Indonesian": "اندونزیایی",
|
||||
"Italian": "ایتالیایی",
|
||||
"Japanese": "ژاپنی",
|
||||
"Kazakh": "قزاقی",
|
||||
"Korean": "کرهای",
|
||||
"Malay": "مالایی",
|
||||
"Persian": "فارسی",
|
||||
|
|
@ -1723,13 +1724,13 @@
|
|||
"Could not establish a secure connection.": "نمیتوان یک اتصال امن ایجاد کرد",
|
||||
"Daily maximum reached.": "به حداکثر ارقام روزانه رسیدید",
|
||||
"Daily sign-in reward": "پاداش ورود روزانه",
|
||||
"Entering tournament...": "ورود به مسابقات...",
|
||||
"Entering tournament...": "ورود به ناورد...",
|
||||
"Higher streaks lead to better rewards.": "رگه های بالاتر منجر به پاداش بهتر می شود.",
|
||||
"Invalid code.": "کد نامعتبر",
|
||||
"Invalid payment; purchase canceled.": "پرداخت نامعتبر؛ خرید لغو شد.",
|
||||
"Invalid promo code.": "کد دعوتی نامعتبر است.",
|
||||
"Invalid purchase.": "خرید نا معتبر",
|
||||
"Invalid tournament entry; score will be ignored.": "ورود مسابقات نامعتبر است؛ نمره نادیده گرفته می شود.",
|
||||
"Invalid tournament entry; score will be ignored.": "ورود نامعتبر به ناورد؛ امتیاز لحاظ نمیشود.",
|
||||
"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(و در عوض داده های این حساب را از دست می دهید)",
|
||||
"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.": ".تعداد نمایهها به حداکثر رسیده است",
|
||||
"Maximum friend code rewards reached.": ".حداکثر جایزه کد ارسالی برای دوستان دریافت شد",
|
||||
"Message is too long.": "پیام خیلی طولانی است",
|
||||
"New tournament result!": "نتیجه مسابقات جدید!",
|
||||
"New tournament result!": "نتیجهی ناورد جدید!",
|
||||
"No servers are available. Please try again soon.": "هیچ سروری در دسترس نیست. لطفا به زودی دوباره امتحان کنید",
|
||||
"No slots available. Free a slot and try again.": "محل صندوقی موجود نیست. یک محل صندوق ی آزاد کنید و دوباره امتحان کنید.",
|
||||
"Profile \"${NAME}\" upgraded successfully.": ".با موفقیت ارتقا یافت «${NAME}» نمایهٔ",
|
||||
|
|
@ -1754,12 +1755,12 @@
|
|||
"Still searching for nearby servers; please try again soon.": "هنوز سرورهای اطراف را جستجو می کنید. لطفا به زودی دوباره امتحان کنید",
|
||||
"Streak: ${NUM} days": "روز ${NUM} دورهی ورود روزانه:",
|
||||
"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 code cannot be used on the account that created it.": "کد در حسابی که با آن ساخته شده قابل استفاده نیست",
|
||||
"This is currently unavailable; please try again later.": ".این گزینه در حال حاضر در دسترس نیست; لطفا بعدا دوباره تلاش کنید",
|
||||
"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}جدید تر یا حرفه ای نیاز دارند",
|
||||
"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در صورت تکرار حساب کاربری شما مسدود خواهد شد! لطفا جوانمردانه بازی کنید.",
|
||||
|
|
@ -1777,12 +1778,13 @@
|
|||
"You got an achievement reward!": "!یه پاداش دستاورد گرفتی",
|
||||
"You have been promoted to a new league; congratulations!": "شما به یک لیگ جدید ارتقا دادهاید؛ تبریک میگوییم!",
|
||||
"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 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 wait a few seconds before entering a new code.": "شما باید چند ثانیه قبل از وارد کردن یک کد جدید صبر کنید.",
|
||||
"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 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لطفا تغییرات رو به حالت اول برگردونید و دوباره امتحان کنید.",
|
||||
|
|
@ -1907,7 +1909,7 @@
|
|||
"phrase12Text": "اگه میخوای یه مشت خفن بزنی که فکش بیاد پایین، باید بدویی و بچرخی.",
|
||||
"phrase13Text": "اوخ! بابت اینکه فکت اومد پایین ببخشید ${NAME} جان.",
|
||||
"phrase14Text": "خیلی چیزها رو میشه برداشت و پرتاب کرد. مثل پرچم... یا ${NAME}",
|
||||
"phrase15Text": "نوبتیم باشه، نوبت بمبهاست.",
|
||||
"phrase15Text": "نوبتی هم باشه، نوبت بمبهاست.",
|
||||
"phrase16Text": "بمب انداختن یه کم تمرین لازم داره.",
|
||||
"phrase17Text": "اوخ اوخ! اصلا پرتاب خوبی نبود.",
|
||||
"phrase18Text": "حرکت کردن باعث میشه تا بتونی دورتر پرتاب کنی.",
|
||||
|
|
@ -1935,9 +1937,9 @@
|
|||
"twoKillText": "دو نفرو با هم کشتی!",
|
||||
"uiScaleText": "UI مقیاس",
|
||||
"unavailableText": "در دسترس نیست",
|
||||
"unclaimedPrizesText": "شما جایزه های بی ادعایی دارید",
|
||||
"unconfiguredControllerDetectedText": ":کنترلر پیکربندی نشده شناسایی شد",
|
||||
"unlockThisInTheStoreText": ". این مورد باید در فروشگاه باز شود",
|
||||
"unclaimedPrizesText": "جایزههای نگرفتهای دارید!",
|
||||
"unconfiguredControllerDetectedText": ":کنترلر پیکربندینشده شناسایی شد",
|
||||
"unlockThisInTheStoreText": "این مورد باید در فروشگاه باز شود.",
|
||||
"unlockThisProfilesText": "برای ایجاد بیش از ${NUM} پروفایل٫ احتیاج به این موارد دارید:",
|
||||
"unlockThisText": ": برا باز کردن قفل این شما نیاز دارید که",
|
||||
"unsupportedControllerText": "متاسفانه کنترلر \"${NAME}\" پشتیبانی نمیشود.",
|
||||
|
|
@ -2007,8 +2009,8 @@
|
|||
"winsPlayerText": "!برنده شد ${NAME}",
|
||||
"winsTeamText": "!برنده شد ${NAME}",
|
||||
"winsText": "!برنده شد ${NAME}",
|
||||
"workspaceSyncErrorText": "خطا در همگامسازی ${WORKSPACE}. برای جزئیات به لاگ مراجعه کنید.",
|
||||
"workspaceSyncReuseText": "نمیتوان ${WORKSPACE} را همگامسازی کرد. استفادهٔ مجدد از نسخهٔ همگامسازیشده قبلی.",
|
||||
"workspaceSyncErrorText": ".برای جزییات لاگ را ببینید .${WORKSPACE} خطا در همگامسازی",
|
||||
"workspaceSyncReuseText": ".را همگامسازی کرد. استفادهٔ مجدد از نسخهٔ همگامسازیشده قبلی ${WORKSPACE} نمیتوان",
|
||||
"worldScoresUnavailableText": "امتیاز های جهانی قابل دسترس نیستند.",
|
||||
"worldsBestScoresText": "بهترین امتیازهای جهانی",
|
||||
"worldsBestTimesText": "بهترین زمان های جهانی",
|
||||
|
|
|
|||
9
dist/ba_data/data/languages/piratespeak.json
vendored
9
dist/ba_data/data/languages/piratespeak.json
vendored
|
|
@ -1620,7 +1620,8 @@
|
|||
"Arabic": "Arrr, Arabic be it!",
|
||||
"Belarussian": "Belarusian Scallywag",
|
||||
"Chinese": "Chinesey Simplified",
|
||||
"ChineseTraditional": "Chinaman's Olde",
|
||||
"ChineseSimplified": "Chinaman's - Simply",
|
||||
"ChineseTraditional": "Chinaman's - Olde",
|
||||
"Croatian": "Croatian lass",
|
||||
"Czech": "Czech, matey!",
|
||||
"Danish": "Danish scallywag",
|
||||
|
|
@ -1638,17 +1639,22 @@
|
|||
"Indonesian": "Indonesian be",
|
||||
"Italian": "Italin'",
|
||||
"Japanese": "Japanese",
|
||||
"Kazakh": "Kazakh, arr!!",
|
||||
"Korean": "Korey",
|
||||
"Malay": "Malaysie",
|
||||
"Persian": "Persian Sea Dog",
|
||||
"PirateSpeak": "Pirate Speak",
|
||||
"Polish": "Polish",
|
||||
"Portuguese": "Portuguese, matey!",
|
||||
"PortugueseBrazil": "Planktuguese - Brazily",
|
||||
"PortuguesePortugal": "Portugalian Portugalese",
|
||||
"Romanian": "Romanian, me hearty!",
|
||||
"Russian": "Russian",
|
||||
"Serbian": "Serbiarr",
|
||||
"Slovak": "Slovak scallywag",
|
||||
"Spanish": "Spanish matey",
|
||||
"SpanishLatinAmerica": "Spanish!! - Latin America",
|
||||
"SpanishSpain": "Spanish!! - Spooin",
|
||||
"Swedish": "Swedisher",
|
||||
"Tamil": "Tamin",
|
||||
"Thai": "Thai be",
|
||||
|
|
@ -1767,6 +1773,7 @@
|
|||
"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 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 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.",
|
||||
|
|
|
|||
13
dist/ba_data/data/languages/polish.json
vendored
13
dist/ba_data/data/languages/polish.json
vendored
|
|
@ -1094,7 +1094,7 @@
|
|||
"macControllerSubsystemMFiText": "Zrobione-dla-iOS/Mac",
|
||||
"macControllerSubsystemTitleText": "Wsparcie Kontrolerów",
|
||||
"mainMenu": {
|
||||
"creditsText": "Info",
|
||||
"creditsText": "Twórcy",
|
||||
"demoMenuText": "Menu Demo",
|
||||
"endGameText": "Koniec Gry",
|
||||
"endTestText": "Zakończ test",
|
||||
|
|
@ -1122,7 +1122,7 @@
|
|||
"modeArcadeText": "Tryb Salonu Gier",
|
||||
"modeClassicText": "Tryb Klasyczny",
|
||||
"modeDemoText": "Tryb Demo",
|
||||
"moreSoonText": "Przybędzie w przyszłości...",
|
||||
"moreSoonText": "Więcej wkrótce...",
|
||||
"mostDestroyedPlayerText": "Najbardziej Zgładzony Gracz",
|
||||
"mostValuablePlayerText": "Najwartościowszy gracz",
|
||||
"mostViolatedPlayerText": "Gracz najbardziej sprofanowany",
|
||||
|
|
@ -1337,7 +1337,7 @@
|
|||
"retryText": "Ponów",
|
||||
"revertText": "Przywróć",
|
||||
"runBoldText": "URUCHOM",
|
||||
"runText": "Uruchom",
|
||||
"runText": "Bieg",
|
||||
"saveText": "Zapisz",
|
||||
"scanScriptsErrorText": "Błąd w skanowaniu skryptów. Sprawdź konsolę dla szczegółów.",
|
||||
"scanScriptsMultipleModulesNeedUpdatesText": "${PATH} oraz ${NUM} innych modułów potrzebują aktualizacji do api ${API}.",
|
||||
|
|
@ -1703,7 +1703,8 @@
|
|||
"Arabic": "Arabski",
|
||||
"Belarussian": "Białoruski",
|
||||
"Chinese": "Chiński Uproszczony",
|
||||
"ChineseTraditional": "Chiński Tradycyjny",
|
||||
"ChineseSimplified": "Chiński - Prosty",
|
||||
"ChineseTraditional": "Chiński - Tradycyjny",
|
||||
"Croatian": "Chorwacki",
|
||||
"Czech": "Czeski",
|
||||
"Danish": "Duński",
|
||||
|
|
@ -1727,11 +1728,15 @@
|
|||
"PirateSpeak": "Piracki język",
|
||||
"Polish": "Polski",
|
||||
"Portuguese": "Portugalski",
|
||||
"PortugueseBrazil": "Portugalski (Brazylia)",
|
||||
"PortuguesePortugal": "Portugalski (Portugalia)",
|
||||
"Romanian": "Rumuński",
|
||||
"Russian": "Rosyjski",
|
||||
"Serbian": "Serbski",
|
||||
"Slovak": "słowacki",
|
||||
"Spanish": "Hiszpański",
|
||||
"SpanishLatinAmerica": "Hiszpański (Ameryka Łacińska)",
|
||||
"SpanishSpain": "Hiszpański (Hiszpania)",
|
||||
"Swedish": "Szwedzki",
|
||||
"Tamil": "Tamil",
|
||||
"Thai": "Tajski",
|
||||
|
|
|
|||
|
|
@ -1065,7 +1065,7 @@
|
|||
"modeClassicText": "Modo Clássico",
|
||||
"modeDemoText": "Modo Demonstração",
|
||||
"moreSoonText": "Mais novidades em breve...",
|
||||
"mostDestroyedPlayerText": "Player Mais Destruído",
|
||||
"mostDestroyedPlayerText": "Jogador Mais Destruído",
|
||||
"mostValuablePlayerText": "Jogador Mais Valioso",
|
||||
"mostViolatedPlayerText": "Jogador Mais Violado",
|
||||
"mostViolentPlayerText": "Jogador Mais Violento",
|
||||
|
|
@ -1075,7 +1075,7 @@
|
|||
"mustInviteFriendsText": "Nota: você deve convidar amigos no\npainel \"${GATHER}\" ou adicionar\ncontroles para jogar no multijogador.",
|
||||
"nameBetrayedText": "${NAME} traiu ${VICTIM}.",
|
||||
"nameDiedText": "${NAME} morreu.",
|
||||
"nameKilledText": "${NAME} espancou ${VICTIM}.",
|
||||
"nameKilledText": "${NAME} matou ${VICTIM}.",
|
||||
"nameNotEmptyText": "Nome não pode estar vazio!",
|
||||
"nameScoresText": "${NAME} fez um ponto!",
|
||||
"nameSuicideKidFriendlyText": "${NAME} acidentalmente morreu.",
|
||||
|
|
@ -1621,6 +1621,7 @@
|
|||
"Indonesian": "Indonésio",
|
||||
"Italian": "Italiano",
|
||||
"Japanese": "Japonês",
|
||||
"Kazakh": "cazaque",
|
||||
"Korean": "Coreano",
|
||||
"Malay": "Malaio",
|
||||
"Persian": "Persa",
|
||||
|
|
@ -1754,6 +1755,7 @@
|
|||
"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 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 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.",
|
||||
|
|
|
|||
|
|
@ -1065,7 +1065,7 @@
|
|||
"modeClassicText": "Modo Clássico",
|
||||
"modeDemoText": "Modo Demonstração",
|
||||
"moreSoonText": "Mais novidades em breve...",
|
||||
"mostDestroyedPlayerText": "Player Mais Destruído",
|
||||
"mostDestroyedPlayerText": "Jogador Mais Destruído",
|
||||
"mostValuablePlayerText": "Jogador Mais Valioso",
|
||||
"mostViolatedPlayerText": "Jogador Mais Violado",
|
||||
"mostViolentPlayerText": "Jogador Mais Violento",
|
||||
|
|
@ -1075,7 +1075,7 @@
|
|||
"mustInviteFriendsText": "Nota: você deve convidar amigos no\npainel \"${GATHER}\" ou adicionar\ncontroles para jogar no multijogador.",
|
||||
"nameBetrayedText": "${NAME} traiu ${VICTIM}.",
|
||||
"nameDiedText": "${NAME} morreu.",
|
||||
"nameKilledText": "${NAME} espancou ${VICTIM}.",
|
||||
"nameKilledText": "${NAME} matou ${VICTIM}.",
|
||||
"nameNotEmptyText": "Nome não pode estar vazio!",
|
||||
"nameScoresText": "${NAME} fez um ponto!",
|
||||
"nameSuicideKidFriendlyText": "${NAME} acidentalmente morreu.",
|
||||
|
|
|
|||
66
dist/ba_data/data/languages/romanian.json
vendored
66
dist/ba_data/data/languages/romanian.json
vendored
|
|
@ -7,6 +7,7 @@
|
|||
"campaignProgressText": "Progres Campanie [Greu]: ${PROGRESS}",
|
||||
"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",
|
||||
"createAnAccountText": "Creează un cont",
|
||||
"customName": "Nume Personalizat",
|
||||
"deleteAccountText": "Șterge contul",
|
||||
"deviceSpecificAccountText": "Foloseşti un cont specific dispozitivului: ${NAME}",
|
||||
|
|
@ -378,6 +379,14 @@
|
|||
"chatMuteText": "Amuțește Chat-ul",
|
||||
"chatMutedText": "Chat-ul Este Amuțit",
|
||||
"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>",
|
||||
"claimText": "Revendică",
|
||||
"codesExplainText": "Codurile sunt furnizate de dezvoltator pentru\ndiagnosticați și corectați problemele legate de cont.",
|
||||
|
|
@ -660,6 +669,8 @@
|
|||
"errorText": "Eroare",
|
||||
"errorUnknownText": "eroare necunoscută",
|
||||
"exitGameText": "Ieși din ${APP_NAME}?",
|
||||
"expiredAgoText": "Expirat acum ${T}",
|
||||
"expiresInText": "Expiră în ${T}",
|
||||
"exportSuccessText": "'${NAME}' a fost exportat cu succes.",
|
||||
"externalStorageText": "Memorie Externă",
|
||||
"failText": "Eșec",
|
||||
|
|
@ -722,6 +733,7 @@
|
|||
"copyCodeConfirmText": "Codul a fost copiat în clipboard.",
|
||||
"copyCodeText": "Copiază Codul",
|
||||
"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?",
|
||||
"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...",
|
||||
|
|
@ -970,12 +982,14 @@
|
|||
"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.",
|
||||
"touchScreenText": "TouchScreen",
|
||||
"unableToCompleteTryAgainText": "Nu se poate finaliza acum.\nTe rugăm să încerci din nou.",
|
||||
"unableToResolveHostText": "Eroare: imposibil de găsit hostul.",
|
||||
"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.",
|
||||
"vrOrientationResetText": "Orientare VR resetată.",
|
||||
"willTimeOutText": "(i se va lua controlul dacă este inactiv)"
|
||||
},
|
||||
"inventoryText": "Inventar",
|
||||
"jumpBoldText": "SARI",
|
||||
"jumpText": "Sari",
|
||||
"keepText": "Păstrează",
|
||||
|
|
@ -1022,7 +1036,9 @@
|
|||
"seasonEndsMinutesText": "Sezonul se sfârșește în ${NUMBER} (de) minute.",
|
||||
"seasonText": "Sezonul ${NUMBER}",
|
||||
"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",
|
||||
"levelBestScoresText": "Cele mai bune scoruri din ${LEVEL}",
|
||||
|
|
@ -1070,6 +1086,8 @@
|
|||
"modeArcadeText": "Mod Pentru Arcade",
|
||||
"modeClassicText": "Modul Clasic",
|
||||
"modeDemoText": "Modul Demonstrativ",
|
||||
"moreSoonText": "Mai multe in curând...",
|
||||
"mostDestroyedPlayerText": "Cel mai distrus jucător",
|
||||
"mostValuablePlayerText": "Cel mai valoros jucător este",
|
||||
"mostViolatedPlayerText": "Cel mai ucis jucător este",
|
||||
"mostViolentPlayerText": "Cel mai violent jucător este",
|
||||
|
|
@ -1120,6 +1138,9 @@
|
|||
"onText": "Pornit",
|
||||
"oneMomentText": "Un Moment...",
|
||||
"onslaughtRespawnText": "${PLAYER} va reveni în valul ${WAVE}",
|
||||
"openMeText": "Deschide-mă!",
|
||||
"openNowText": "Deschide Acum",
|
||||
"openText": "Deschide",
|
||||
"orText": "${A} sau ${B}",
|
||||
"otherText": "Altele...",
|
||||
"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."
|
||||
},
|
||||
"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",
|
||||
"replayEndText": "Sfârşeşte Reluarea",
|
||||
"replayNameDefaultText": "Reluarea Ultimului Joc",
|
||||
|
|
@ -1374,6 +1396,7 @@
|
|||
},
|
||||
"spaceKeyText": "spacebar",
|
||||
"statsText": "Statistici",
|
||||
"stopRemindingMeText": "Nu-mi Mai Reaminti",
|
||||
"storagePermissionAccessText": "Acest beneficiu are nevoie de permisiuni de stocare",
|
||||
"store": {
|
||||
"alreadyOwnText": "Ai cumpărat ${NAME} deja!",
|
||||
|
|
@ -1435,6 +1458,7 @@
|
|||
"testBuildValidatedText": "Versiune de Test validată; bucură-te de ea!",
|
||||
"thankYouText": "Îți mulțumesc pentru suport! Bucură-te de joc!!",
|
||||
"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",
|
||||
"timeElapsedText": "Timp Scurs",
|
||||
"timeExpiredText": "Timp Expirat",
|
||||
|
|
@ -1449,17 +1473,20 @@
|
|||
"getTokensText": "Obțineți jetoane",
|
||||
"notEnoughTokensText": "Insuficiente 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.",
|
||||
"tokenPack1Text": "Pachet mic de jetoane",
|
||||
"tokenPack2Text": "Pachet mediu de jetoane",
|
||||
"tokenPack3Text": "Pachet mare de jetoane",
|
||||
"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ă!"
|
||||
},
|
||||
"topFriendsText": "Prieteni de Top",
|
||||
"tournamentCheckingStateText": "Se verifică starea campionatului; aşteaptă...",
|
||||
"tournamentEndedText": "Acest turneu s-a terminat. Altul nou va începe în curând.",
|
||||
"tournamentEntryText": "Prețul Pentru Intrare",
|
||||
"tournamentFinalStandingsText": "Clasamentul final",
|
||||
"tournamentResultsRecentText": "Rezultatele Recente ale Turneului",
|
||||
"tournamentStandingsText": "Clasamentele Turneului",
|
||||
"tournamentText": "Turneu",
|
||||
|
|
@ -1515,6 +1542,18 @@
|
|||
"Uber Onslaught": "MEGA Măcel",
|
||||
"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": {
|
||||
"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.",
|
||||
|
|
@ -1598,7 +1637,8 @@
|
|||
"Arabic": "Arabă",
|
||||
"Belarussian": "Belarusă",
|
||||
"Chinese": "Chineză (Simplificată)",
|
||||
"ChineseTraditional": "Chineză (Tradițională)",
|
||||
"ChineseSimplified": "Chineză - Simplificată",
|
||||
"ChineseTraditional": "Chineză - Tradițională",
|
||||
"Croatian": "Croată",
|
||||
"Czech": "Cehă",
|
||||
"Danish": "Daneză",
|
||||
|
|
@ -1622,11 +1662,15 @@
|
|||
"PirateSpeak": "Limba Piraților",
|
||||
"Polish": "Poloneză",
|
||||
"Portuguese": "Portugheză",
|
||||
"PortugueseBrazil": "Portugheză - Brazilia",
|
||||
"PortuguesePortugal": "Portugheză - Portugalia",
|
||||
"Romanian": "Română",
|
||||
"Russian": "Rusă",
|
||||
"Serbian": "Sârbă",
|
||||
"Slovak": "Slovacă",
|
||||
"Spanish": "Spaniolă",
|
||||
"SpanishLatinAmerica": "Spaniolă - America Latină",
|
||||
"SpanishSpain": "Spaniolă - Spania",
|
||||
"Swedish": "Suedeză",
|
||||
"Tamil": "Tamilă",
|
||||
"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.",
|
||||
"Could not establish a secure connection.": "Nu s-a putut stabili o conexiune sigură.",
|
||||
"Daily maximum reached.": "Limită Zilnică Atinsă.",
|
||||
"Daily sign-in reward": "Recompensă zilnică pentru conectare",
|
||||
"Entering tournament...": "Se intră în turneu...",
|
||||
"Invalid code.": "Cod Invalid.",
|
||||
"Invalid payment; purchase canceled.": "Plată invalidă; achiziționare anulată.",
|
||||
|
|
@ -1700,11 +1745,14 @@
|
|||
"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)",
|
||||
"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 profiles reached.": "Numărul maxim de profile a fost atins.",
|
||||
"Maximum friend code rewards reached.": "Limita codurilor pentru prieteni a fost atinsă.",
|
||||
"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 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 could not be upgraded.": "Profilul nu a putut fi îmbunătăț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 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 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.",
|
||||
"Streak: ${NUM} days": "Serie: ${NUM} zile",
|
||||
"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.",
|
||||
"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",
|
||||
"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.",
|
||||
"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!",
|
||||
"You already own this!": "Deja ai acest lucru!",
|
||||
"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 own that.": "Nu deți asta.",
|
||||
"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 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 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 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 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!",
|
||||
"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 friend code was used by ${ACCOUNT}": "${ACCOUNT} a folosit codul tău de prieten"
|
||||
},
|
||||
|
|
@ -1890,6 +1948,7 @@
|
|||
"twoKillText": "DUBLU OMOR!",
|
||||
"uiScaleText": "Mărime Interfață",
|
||||
"unavailableText": "indisponibil",
|
||||
"unclaimedPrizesText": "Ai premii ne revendicate!",
|
||||
"unconfiguredControllerDetectedText": "Controller neconfigurat detectat:",
|
||||
"unlockThisInTheStoreText": "Acest lucru trebuie deblocat din magazin.",
|
||||
"unlockThisProfilesText": "Că să creezi mai mult de ${NUM} profile, va trebui să ai:",
|
||||
|
|
@ -1976,5 +2035,6 @@
|
|||
},
|
||||
"yesAllowText": "Da, Permite!",
|
||||
"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:"
|
||||
}
|
||||
4
dist/ba_data/data/languages/russian.json
vendored
4
dist/ba_data/data/languages/russian.json
vendored
|
|
@ -1725,6 +1725,7 @@
|
|||
"Indonesian": "Индонезийский",
|
||||
"Italian": "Итальянский",
|
||||
"Japanese": "Японский",
|
||||
"Kazakh": "казахский",
|
||||
"Korean": "Корейский",
|
||||
"Malay": "Малайский",
|
||||
"Persian": "Персидский",
|
||||
|
|
@ -1861,6 +1862,7 @@
|
|||
"You got an achievement reward!": "Вы получили награду за достижение!",
|
||||
"You have been promoted to a new league; congratulations!": "Вас повысили и перевели в новую лигу; поздравляем!",
|
||||
"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 to a newer version of the app 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 you are short on controllers, install the '${REMOTE_APP_NAME}' app\non your mobile devices to use them as controllers.": "Если вам не хватает контроллеров, установите приложение '${REMOTE_APP_NAME}' \nна ваши мобильные устройства, чтобы использовать их в качестве контроллеров.",
|
||||
"If you are short on controllers, install the 'BombSquad Remote' app\non your iOS or Android devices to use them as controllers.": "Если не хватает контроллеров, установите приложение 'BombSquad Remote' на\nустройства iOS или Android, чтобы использовать их в качестве контроллеров.",
|
||||
"If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "Если к вам прилипла липкая бомба, прыгайте и крутитесь. Может повезет\nстряхнуть бомбу или, на худой конец, повеселить окружающих.",
|
||||
"If you get a sticky-bomb stuck to you, jump around and spin in circles. You might\nshake the bomb off, or if nothing else your last moments will be entertaining.": "Если к вам прилипла бомба-липучка, прыгайте и крутитесь. Может повезет\nстряхнуть бомбу или, на худой конец, повеселить окружающих.",
|
||||
"If you kill an enemy in one hit you get double points for it.": "Если убиваешь врага с одного удара, то получаешь двойные очки.",
|
||||
"If you pick up a curse, your only hope for survival is to\nfind a health powerup in the next few seconds.": "Если подхватили проклятие, то единственная надежда на выживание\n- это найти аптечку в ближайшие несколько секунд.",
|
||||
"If you stay in one place, you're toast. Run and dodge to survive..": "Не стой на месте – помрешь. Беги и уворачивайся чтобы выжить..",
|
||||
|
|
|
|||
110
dist/ba_data/data/languages/slovak.json
vendored
110
dist/ba_data/data/languages/slovak.json
vendored
|
|
@ -7,6 +7,7 @@
|
|||
"campaignProgressText": "Priebeh kampane [Hard] : ${PROGRESS}",
|
||||
"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)",
|
||||
"createAnAccountText": "Vytvoriť si Účet",
|
||||
"customName": "Vlastný názov",
|
||||
"deleteAccountText": "Zmazať Účet",
|
||||
"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",
|
||||
"chatMutedText": "Čet Zablokovaný",
|
||||
"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>",
|
||||
"claimText": "Získať",
|
||||
"codesExplainText": "Kódy sú poskytované vývojárovi \nk diagnostike a oprave problémov s účtom.",
|
||||
"completeThisLevelToProceedText": "Musíš dokončiť\ntento level pre postup!",
|
||||
"completionBonusText": "Bonus za Dokončenie",
|
||||
|
|
@ -657,6 +667,8 @@
|
|||
"errorText": "Chyba",
|
||||
"errorUnknownText": "neznámy error",
|
||||
"exitGameText": "Ukončiť ${APP_NAME}?",
|
||||
"expiredAgoText": "Vypršalo pred ${T}",
|
||||
"expiresInText": "Vyprší za ${T}",
|
||||
"exportSuccessText": "\"${NAME}\" exportovaný.",
|
||||
"externalStorageText": "Externé Úložisko",
|
||||
"failText": "Zlyhanie",
|
||||
|
|
@ -718,6 +730,7 @@
|
|||
"copyCodeConfirmText": "Kód bol skopírovaný do schránky.",
|
||||
"copyCodeText": "Kopírovať kód",
|
||||
"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ý?",
|
||||
"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...",
|
||||
|
|
@ -730,7 +743,7 @@
|
|||
"friendHasSentPromoCodeText": "${COUNT} ${APP_NAME} tiketov od ${NAME}",
|
||||
"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.",
|
||||
"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í.",
|
||||
"friendPromoCodeRedeemShortText": "Môže byť uplatnený za ${COUNT} tiketov v hre.",
|
||||
"friendPromoCodeWhereToEnterText": "(V časti „Nastavenia-> Pokročilé-> Poslať info\")",
|
||||
|
|
@ -909,6 +922,7 @@
|
|||
"importText": "Importovať",
|
||||
"importingText": "Importujem...",
|
||||
"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.",
|
||||
"internal": {
|
||||
"arrowsToExitListText": "stlač ${LEFT} alebo ${RIGHT} pre zatvorenie listu",
|
||||
|
|
@ -963,12 +977,14 @@
|
|||
"timeOutText": "(vyprší za ${TIME} sekúnd)",
|
||||
"touchScreenJoinWarningText": "Pripojil si sa na obrazovke.\nAk to bolo nechtiac, stlač na obrazovke Menu->Opustiť Hru.",
|
||||
"touchScreenText": "Obrazovka",
|
||||
"unableToCompleteTryAgainText": "Nemožno to teraz dokončiť.\nSkúste to znova.",
|
||||
"unableToResolveHostText": "Error: žiadny internet/zlé host meno.",
|
||||
"unavailableNoConnectionText": "Toto nie je aktuálne dostupné (žiadny internet?)",
|
||||
"vrOrientationResetCardboardText": "Toto použi pre resetovanie orientácie VR.\nAk chceš hrať, potrebuješ ovládač.",
|
||||
"vrOrientationResetText": "Orientácia VR resetovaná.",
|
||||
"willTimeOutText": "(ak bude nečinný, vyprší čas)"
|
||||
},
|
||||
"inventoryText": "Inventár",
|
||||
"jumpBoldText": "SKOČIŤ",
|
||||
"jumpText": "Skočiť",
|
||||
"keepText": "Ponechať",
|
||||
|
|
@ -1015,8 +1031,11 @@
|
|||
"seasonEndsMinutesText": "Sezóna skončí za ${NUMBER} minút.",
|
||||
"seasonText": "Sezóna ${NUMBER}",
|
||||
"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}",
|
||||
"levelBestTimesText": "Najlepšie časy na ${LEVEL}",
|
||||
"levelIsLockedText": "${LEVEL} je zamknutý.",
|
||||
|
|
@ -1060,6 +1079,8 @@
|
|||
"modeArcadeText": "Arkádový režim",
|
||||
"modeClassicText": "Klasický režim",
|
||||
"modeDemoText": "Demo režim",
|
||||
"moreSoonText": "Viac už čoskoro...",
|
||||
"mostDestroyedPlayerText": "Najviac Zabitý Hráč",
|
||||
"mostValuablePlayerText": "Najcennejší Hráč",
|
||||
"mostViolatedPlayerText": "Najzomierajúcejší Hráč",
|
||||
"mostViolentPlayerText": "Najvražednejší Hráč",
|
||||
|
|
@ -1096,6 +1117,7 @@
|
|||
"noValidMapsErrorText": "Žiadne platné mapy sa pre tento typ hry nenašli.",
|
||||
"notEnoughPlayersRemainingText": "Nedostatok hráčov; začni novú hru.",
|
||||
"notEnoughPlayersText": "Potrebuješ aspoň ${COUNT} hráčov ak chceš toto hrať!",
|
||||
"notEnoughTicketsText": "Nedostatok ticketov!",
|
||||
"notNowText": "Teraz Nie",
|
||||
"notSignedInErrorText": "Ak toto chceš urobiť, musíš sa prihlásiť.",
|
||||
"notSignedInGooglePlayErrorText": "Ak chceš toto urobiť, musíš sa prihlásiť do Google Play.",
|
||||
|
|
@ -1108,6 +1130,9 @@
|
|||
"onText": "Zapnúť",
|
||||
"oneMomentText": "Chvíľu...",
|
||||
"onslaughtRespawnText": "${PLAYER} sa znova zjaví vo vlne ${WAVE}",
|
||||
"openMeText": "Otvor Ma!",
|
||||
"openNowText": "Otvoriť Teraz",
|
||||
"openText": "Otvoriť",
|
||||
"orText": "${A} alebo ${B}",
|
||||
"otherText": "Ďalšie...",
|
||||
"outOfText": "(#${RANK} z ${ALL})",
|
||||
|
|
@ -1193,6 +1218,8 @@
|
|||
"punchText": "Úder",
|
||||
"purchaseForText": "Kúpiť za ${PRICE}",
|
||||
"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...",
|
||||
"quitGameText": "Ukončiť ${APP_NAME}?",
|
||||
"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."
|
||||
},
|
||||
"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ť",
|
||||
"replayEndText": "Ukončiť Replay",
|
||||
"replayNameDefaultText": "Replay Poslednej Hry",
|
||||
|
|
@ -1267,6 +1295,7 @@
|
|||
},
|
||||
"scoreWasText": "(predtým ${COUNT})",
|
||||
"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",
|
||||
"seriesWinLine1TeamText": "VYHRÁVA",
|
||||
"seriesWinLine1Text": "VYHRÁVA",
|
||||
|
|
@ -1284,6 +1313,7 @@
|
|||
"alwaysUseInternalKeyboardDescriptionText": "(jednoduchá, podporujúca-ovládač na-obrazovke-klávesnica pre písanie textu)",
|
||||
"alwaysUseInternalKeyboardText": "Stále používať klávesnicu v programe",
|
||||
"benchmarksText": "Benchmarky & Stres-Testy",
|
||||
"devToolsText": "Nástroje Pre Vývojára",
|
||||
"disableCameraGyroscopeMotionText": "Zakázať pohyb gyroskopu fotoaparátu",
|
||||
"disableCameraShakeText": "Zakázať otrasy fotoaparátu",
|
||||
"disableThisNotice": "(toto upozornenie môžeš vypnúť v Pokročilých nastaveniach)",
|
||||
|
|
@ -1292,17 +1322,22 @@
|
|||
"enterPromoCodeText": "Vložiť Kód",
|
||||
"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!",
|
||||
"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ď.)",
|
||||
"languageText": "Jazyk",
|
||||
"moddingGuideText": "Tutoriál pre Módovanie",
|
||||
"moddingToolsText": "Módovacie Nástroje",
|
||||
"mustRestartText": "Ak chceš toto nastavenie použiť, musíš reštartovať hru.",
|
||||
"netTestingText": "Testovanie Internetu",
|
||||
"resetText": "Resetovať",
|
||||
"sendInfoText": "Poslať Info",
|
||||
"showBombTrajectoriesText": "Ukázovať Trajektóriu Bomby",
|
||||
"showDemosWhenIdleText": "Zobraziť Ukážky Pri Nečinnosti",
|
||||
"showDevConsoleButtonText": "Zobraziť Tlačidlo Konzoly Zariadenia",
|
||||
"showInGamePingText": "Ukázať Ping v Hre",
|
||||
"showDemosWhenIdleText": "Zobraziť ukážky pri nečinnosti",
|
||||
"showDeprecatedLoginTypesText": "Ukázať zastarané spôsoby prihlásenia",
|
||||
"showDevConsoleButtonText": "Zobraziť tlačidlo konzoly pre vývojára",
|
||||
"showInGamePingText": "Ukázať ping v hre",
|
||||
"showPlayerNamesText": "Ukazovať Mená Hráčov",
|
||||
"showUserModsText": "Ukázať Zložku pre Módy",
|
||||
"titleText": "Pokročilé",
|
||||
|
|
@ -1310,8 +1345,8 @@
|
|||
"translationFetchErrorText": "status pre jazyk nedostupný",
|
||||
"translationFetchingStatusText": "kontrolujem status jazyka...",
|
||||
"translationInformMe": "Informuj ma keď môj jazyk potrebuje vylepšiť",
|
||||
"translationNoUpdateNeededText": "Slovenčina je celá preložená, jupiii!",
|
||||
"translationUpdateNeededText": "** Slovenčina potrebuje dokončiť!! **",
|
||||
"translationNoUpdateNeededText": "Slovenský preklad je kompletný, jupiii!",
|
||||
"translationUpdateNeededText": "** Slovenčina potrebuje dokončiť preklad!! **",
|
||||
"vrTestingText": "VR Testovanie"
|
||||
},
|
||||
"shareText": "Zdieľať",
|
||||
|
|
@ -1349,6 +1384,7 @@
|
|||
},
|
||||
"spaceKeyText": "medzerník",
|
||||
"statsText": "Štatistiky",
|
||||
"stopRemindingMeText": "Prestaň Mi Pripomínať",
|
||||
"storagePermissionAccessText": "Na toto potrebuješ povolenie k úložisku",
|
||||
"store": {
|
||||
"alreadyOwnText": "Už vlastníš ${NAME}!",
|
||||
|
|
@ -1407,6 +1443,7 @@
|
|||
"testBuildValidatedText": "Testovacia Verzia Platná; Uži si to!",
|
||||
"thankYouText": "Vďaka za podporu! Uži si hru!!",
|
||||
"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",
|
||||
"timeElapsedText": "Čas Uplynul",
|
||||
"timeExpiredText": "Čas Uplynul",
|
||||
|
|
@ -1417,10 +1454,24 @@
|
|||
"tipText": "Tip",
|
||||
"titleText": "BombSquad",
|
||||
"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",
|
||||
"tournamentCheckingStateText": "Kontrolujem štádium turnaju; prosím počkaj...",
|
||||
"tournamentEndedText": "Tento turnaj skončil. Nový začne o chvíľu.",
|
||||
"tournamentEntryText": "Vstupné",
|
||||
"tournamentFinalStandingsText": "Konečné Poradie",
|
||||
"tournamentResultsRecentText": "Nedávne Výsledky Turnaja",
|
||||
"tournamentStandingsText": "Postavenie v Turnaji",
|
||||
"tournamentText": "Turnaj",
|
||||
|
|
@ -1476,6 +1527,18 @@
|
|||
"Uber Onslaught": "Extrémny Útok",
|
||||
"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": {
|
||||
"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.",
|
||||
|
|
@ -1559,7 +1622,8 @@
|
|||
"Arabic": "Arabčina",
|
||||
"Belarussian": "Bieloruština",
|
||||
"Chinese": "Zjednodušená Čínština",
|
||||
"ChineseTraditional": "Tradičná Čínština",
|
||||
"ChineseSimplified": "Čínsky - Zjednodušená",
|
||||
"ChineseTraditional": "Čínština - Tradičná",
|
||||
"Croatian": "Chorváčtina",
|
||||
"Czech": "Čeština",
|
||||
"Danish": "Dánčina",
|
||||
|
|
@ -1580,13 +1644,18 @@
|
|||
"Korean": "Kórejčina",
|
||||
"Malay": "Malajčina",
|
||||
"Persian": "Perzština",
|
||||
"PirateSpeak": "Pirátske rozprávanie",
|
||||
"Polish": "Poľština",
|
||||
"Portuguese": "Portugálčina",
|
||||
"PortugueseBrazil": "Portugalsky - Brazília",
|
||||
"PortuguesePortugal": "Portugalsky - Portugalsko",
|
||||
"Romanian": "Rumunčina",
|
||||
"Russian": "Ruština",
|
||||
"Serbian": "Srbčina",
|
||||
"Slovak": "Slovenčina",
|
||||
"Spanish": "Španielčina",
|
||||
"SpanishLatinAmerica": "Španielsky - Latinská Amerika",
|
||||
"SpanishSpain": "Španielsky - Španielsko",
|
||||
"Swedish": "Švédčina",
|
||||
"Tamil": "Tamilčina",
|
||||
"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.",
|
||||
"Could not establish a secure connection.": "Nepodarilo sa vytvoriť zabezpečené pripojenie.",
|
||||
"Daily maximum reached.": "Denný maximum dosiahnutý.",
|
||||
"Daily sign-in reward": "Denná odmena",
|
||||
"Entering tournament...": "Vstupujem do turnaja...",
|
||||
"Invalid code.": "Nesprávny kód.",
|
||||
"Invalid payment; purchase canceled.": "Neplatná platba; nákup zrušený.",
|
||||
|
|
@ -1660,11 +1730,14 @@
|
|||
"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).",
|
||||
"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 profiles reached.": "Maximálny počet profilov 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á.",
|
||||
"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 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 could not be upgraded.": "Profil nemožno vylepšiť.",
|
||||
"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 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 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.",
|
||||
"Streak: ${NUM} days": "Séria: ${NUM} dní",
|
||||
"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.",
|
||||
"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.",
|
||||
"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.",
|
||||
"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ť!",
|
||||
"You already own this!": "Toto už vlastníš!",
|
||||
"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 own that.": "Toto ešte nevlastníš.",
|
||||
"You got ${COUNT} tickets!": "Máš ${COUNT} tiketov!",
|
||||
"You got ${COUNT} tokens!": "Máš ${COUNT} tokenov!",
|
||||
"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 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 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 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!",
|
||||
"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 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)"
|
||||
},
|
||||
"twoKillText": "DOUBLE-KILL!",
|
||||
"uiScaleText": "Veľkosť UI",
|
||||
"unavailableText": "nedostupné",
|
||||
"unclaimedPrizesText": "Máš nevyzdvihnuté darčeky!",
|
||||
"unconfiguredControllerDetectedText": "Nenastavený ovládač detekovaný:",
|
||||
"unlockThisInTheStoreText": "Toto musí byť odomknuté v obchode.",
|
||||
"unlockThisProfilesText": "Ak chceš vytvoriť viac ako ${NUM} profilov, potrebuješ:",
|
||||
|
|
@ -1859,9 +1945,12 @@
|
|||
"upgradeText": "Vylepšiť",
|
||||
"upgradeToPlayText": "Odomkni \"${PRO}\" v obchode (v hre) ak toto chceš hrať.",
|
||||
"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č.",
|
||||
"usingItunesText": "Používam Music App ako soundtrack...",
|
||||
"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...",
|
||||
"viaText": "ako",
|
||||
"victoryText": "Výhra!",
|
||||
|
|
@ -1928,5 +2017,6 @@
|
|||
},
|
||||
"yesAllowText": "Áno, Povoliť!",
|
||||
"yourBestScoresText": "Tvoje Najlepšie Skóre",
|
||||
"yourBestTimesText": "Tvoje Najlepšie Časy"
|
||||
"yourBestTimesText": "Tvoje Najlepšie Časy",
|
||||
"yourPrizeText": "Tvoja odmena:"
|
||||
}
|
||||
|
|
@ -1,27 +1,27 @@
|
|||
{
|
||||
"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",
|
||||
"achievementProgressText": "Logros: ${COUNT} de ${TOTAL}",
|
||||
"campaignProgressText": "Progreso de la Campaña [Difícil]: ${PROGRESS}",
|
||||
"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)",
|
||||
"createAnAccountText": "Crear una Cuenta",
|
||||
"changeOncePerSeasonError": "Deberás esperar hasta la siguiente temporada para cambiar esto otra vez (${NUM} día/s)",
|
||||
"createAnAccountText": "Crea una Cuenta",
|
||||
"customName": "Nombre Personalizado",
|
||||
"deleteAccountText": "Eliminar Cuenta",
|
||||
"googlePlayGamesAccountSwitchText": "Si quieres usar una cuenta de Google diferente,\nusa la app Google Play Juegos para cambiarla.",
|
||||
"linkAccountsEnterCodeText": "Introducir Código",
|
||||
"deleteAccountText": "Borrar Cuenta",
|
||||
"googlePlayGamesAccountSwitchText": "Si Queres Usar una Cuenta diferente, usa la d Google Play Juegos para cambiarla.\n(NOTA: Ya No c si funciona)",
|
||||
"linkAccountsEnterCodeText": "Ingresar Código",
|
||||
"linkAccountsGenerateCodeText": "Generar Código",
|
||||
"linkAccountsInfoText": "(comparta el progreso a través de 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.",
|
||||
"linkAccountsInfoText": "(comparte tu progreso con diferentes plataformas)",
|
||||
"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",
|
||||
"linkedAccountsText": "Cuentas Vinculadas:",
|
||||
"manageAccountText": "Administrar Cuenta",
|
||||
"nameChangeConfirm": "¿Quieres 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?",
|
||||
"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?",
|
||||
"nameChangeConfirm": "¿Cambiar el nombre de tu cuenta a ${NAME}?",
|
||||
"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 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",
|
||||
"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.",
|
||||
"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",
|
||||
|
|
@ -1576,7 +1576,7 @@
|
|||
},
|
||||
"gameNames": {
|
||||
"Assault": "Asalto",
|
||||
"Capture the Flag": "Captura la Bandera",
|
||||
"Capture the Flag": "Captura De Banderas",
|
||||
"Chosen One": "El Elegido",
|
||||
"Conquest": "Conquista",
|
||||
"Death Match": "Combate Mortal",
|
||||
|
|
@ -1621,6 +1621,7 @@
|
|||
"Indonesian": "Indonesio",
|
||||
"Italian": "Italiano",
|
||||
"Japanese": "Japonés",
|
||||
"Kazakh": "Kazajo",
|
||||
"Korean": "Coreano",
|
||||
"Malay": "Malayo",
|
||||
"Persian": "Persa",
|
||||
|
|
@ -1754,6 +1755,7 @@
|
|||
"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 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 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.",
|
||||
|
|
@ -1786,7 +1788,7 @@
|
|||
"Entire Team Must Finish": "Todo el Equipo Debe Terminar",
|
||||
"Epic Mode": "Modo Épico",
|
||||
"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",
|
||||
"Kills to Win Per Player": "Asesinatos para Ganar Por Jugador",
|
||||
"Laps": "Vueltas",
|
||||
|
|
|
|||
|
|
@ -1621,6 +1621,7 @@
|
|||
"Indonesian": "Indonesio",
|
||||
"Italian": "Italiano",
|
||||
"Japanese": "Japonés",
|
||||
"Kazakh": "Kazajo",
|
||||
"Korean": "Coreano",
|
||||
"Malay": "Malayo",
|
||||
"Persian": "Persa",
|
||||
|
|
|
|||
1
dist/ba_data/data/languages/swedish.json
vendored
1
dist/ba_data/data/languages/swedish.json
vendored
|
|
@ -7,6 +7,7 @@
|
|||
"campaignProgressText": "Kampanjutveckling: [svår] ${PROGRESS}",
|
||||
"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)",
|
||||
"createAnAccountText": "Skapa ett konto",
|
||||
"customName": "Anpassat Namn",
|
||||
"deleteAccountText": "Ta bort konto",
|
||||
"googlePlayGamesAccountSwitchText": "Om du vill använda ett annat Google-konto,\nanvänd appen Google Play Spel för att byta.",
|
||||
|
|
|
|||
8
dist/ba_data/data/languages/turkish.json
vendored
8
dist/ba_data/data/languages/turkish.json
vendored
|
|
@ -1620,7 +1620,8 @@
|
|||
"Arabic": "Arapça",
|
||||
"Belarussian": "Beyazrusça",
|
||||
"Chinese": "Basitleştirilmiş Çince",
|
||||
"ChineseTraditional": "Geleneksel Çince",
|
||||
"ChineseSimplified": "Çince basitleştirilmiş",
|
||||
"ChineseTraditional": "Geleneksel çince",
|
||||
"Croatian": "Hırvatça",
|
||||
"Czech": "Çekçe",
|
||||
"Danish": "Danimarkaca",
|
||||
|
|
@ -1638,17 +1639,22 @@
|
|||
"Indonesian": "Endonezyaca",
|
||||
"Italian": "İtalyanca",
|
||||
"Japanese": "Japonca",
|
||||
"Kazakh": "Kazakça",
|
||||
"Korean": "Korece",
|
||||
"Malay": "Malayca",
|
||||
"Persian": "Farsça",
|
||||
"PirateSpeak": "Korsan Dili",
|
||||
"Polish": "Polonya Dili",
|
||||
"Portuguese": "Portekizce",
|
||||
"PortugueseBrazil": "Portekizce (brezilya)",
|
||||
"PortuguesePortugal": "Portekizce (portekiz)",
|
||||
"Romanian": "Romence",
|
||||
"Russian": "Rusça",
|
||||
"Serbian": "Sırpça",
|
||||
"Slovak": "Slovakça",
|
||||
"Spanish": "İspanyolca",
|
||||
"SpanishLatinAmerica": "İspanyolca (latin amerika)",
|
||||
"SpanishSpain": "İspanyolca (ispanya)",
|
||||
"Swedish": "İsveççe",
|
||||
"Tamil": "Tamilce",
|
||||
"Thai": "Tayland dili",
|
||||
|
|
|
|||
9
dist/ba_data/data/languages/ukrainian.json
vendored
9
dist/ba_data/data/languages/ukrainian.json
vendored
|
|
@ -1620,7 +1620,8 @@
|
|||
"Arabic": "Арабська",
|
||||
"Belarussian": "Білоруська",
|
||||
"Chinese": "Китайська",
|
||||
"ChineseTraditional": "Китайська традиційна",
|
||||
"ChineseSimplified": "Китайська - Спрощена",
|
||||
"ChineseTraditional": "Китайська - Традиційна",
|
||||
"Croatian": "Хорватська",
|
||||
"Czech": "Чеська",
|
||||
"Danish": "Данська",
|
||||
|
|
@ -1638,17 +1639,22 @@
|
|||
"Indonesian": "Індонезійська",
|
||||
"Italian": "Італійська",
|
||||
"Japanese": "Японська",
|
||||
"Kazakh": "Казахська",
|
||||
"Korean": "Корейська",
|
||||
"Malay": "Малайська",
|
||||
"Persian": "Перська",
|
||||
"PirateSpeak": "Піратська мова",
|
||||
"Polish": "Польська",
|
||||
"Portuguese": "Португальська",
|
||||
"PortugueseBrazil": "Португальська - Бразилія",
|
||||
"PortuguesePortugal": "Португальська - Португалія",
|
||||
"Romanian": "Румунська",
|
||||
"Russian": "Російська",
|
||||
"Serbian": "Сербська",
|
||||
"Slovak": "Словацька",
|
||||
"Spanish": "Іспанська",
|
||||
"SpanishLatinAmerica": "Іспанська - Латинська Америка",
|
||||
"SpanishSpain": "Іспанська - Іспанія",
|
||||
"Swedish": "Шведська",
|
||||
"Tamil": "тамільська",
|
||||
"Thai": "Тайська",
|
||||
|
|
@ -1767,6 +1773,7 @@
|
|||
"You got an achievement reward!": "Ви отримали нагороду за досягнення!",
|
||||
"You have been promoted to a new league; congratulations!": "Вас підвищили і перевели в нову лігу; вітаємо!",
|
||||
"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 to a newer version of the app to do this.": "Щоб це зробити, ви повинні оновити додаток.",
|
||||
"You must update to the newest version of the game to do this.": "Ви повинні оновитися до нової версії гри, щоб зробити це.",
|
||||
|
|
|
|||
7
dist/ba_data/data/languages/vietnamese.json
vendored
7
dist/ba_data/data/languages/vietnamese.json
vendored
|
|
@ -1622,7 +1622,8 @@
|
|||
"Arabic": "Tiếng Ả Rập",
|
||||
"Belarussian": "Tiếng Belarus",
|
||||
"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",
|
||||
"Czech": "Tiếng Séc",
|
||||
"Danish": "Tiếng Đan Mạch",
|
||||
|
|
@ -1646,11 +1647,15 @@
|
|||
"PirateSpeak": "Tiếng Hải tặc",
|
||||
"Polish": "Tiếng Polish",
|
||||
"Portuguese": "Tiếng Bồ Đào Nha",
|
||||
"PortugueseBrazil": "Bồ Đào Nha - Brazil",
|
||||
"PortuguesePortugal": "Tiếng Bồ Đào Nha - Bồ Đào Nha",
|
||||
"Romanian": "Tiếng Rumani",
|
||||
"Russian": "Tiếng Nga",
|
||||
"Serbian": "Tiếng Serbia",
|
||||
"Slovak": "Tiếng Slovakia",
|
||||
"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",
|
||||
"Tamil": "Tamil",
|
||||
"Thai": "Tiếng thái",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from .core import contents, where
|
||||
|
||||
__all__ = ["contents", "where"]
|
||||
__version__ = "2025.01.31"
|
||||
__version__ = "2025.08.03"
|
||||
|
|
|
|||
363
dist/ba_data/python-site-packages/certifi/cacert.pem
vendored
363
dist/ba_data/python-site-packages/certifi/cacert.pem
vendored
|
|
@ -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.
|
||||
# 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"
|
||||
|
|
@ -125,39 +34,6 @@ eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m
|
|||
0vdXcDazv/wor3ElhVsT/h5/WrQ8
|
||||
-----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
|
||||
# Subject: CN=QuoVadis Root CA 2 O=QuoVadis Limited
|
||||
# Label: "QuoVadis Root CA 2"
|
||||
|
|
@ -245,103 +121,6 @@ mJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK
|
|||
4SVhM7JZG+Ju1zdXtg2pEto=
|
||||
-----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
|
||||
# Subject: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com
|
||||
# Label: "DigiCert Assured ID Root CA"
|
||||
|
|
@ -3371,46 +3150,6 @@ DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ
|
|||
+RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A=
|
||||
-----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
|
||||
# Subject: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz
|
||||
# Label: "ANF Secure Server Root CA"
|
||||
|
|
@ -4855,6 +4594,68 @@ knCDgKs4qllo3UCkGJCy89UDyibK79XH4I9TjvAA46jtn/mtd+ArY0+ew+43u3gJ
|
|||
hJ65bvspmZDogNOfJA==
|
||||
-----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
|
||||
# Subject: CN=D-TRUST EV Root CA 2 2023 O=D-Trust GmbH
|
||||
# Label: "D-TRUST EV Root CA 2 2023"
|
||||
|
|
@ -4895,3 +4696,43 @@ gofXNJhuS5N5YHVpD/Aa1VP6IQzCP+k/HxiMkl14p3ZnGbuy6n/pcAlWVqOwDAst
|
|||
Nl7F6cTVg8uGF5csbBNvh1qvSaYd2804BC5f4ko1Di1L+KIkBI3Y4WNeApI02phh
|
||||
XBxvWHZks/wCuPWdCg==
|
||||
-----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-----
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ if sys.version_info >= (3, 11):
|
|||
def contents() -> str:
|
||||
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
|
||||
|
||||
|
|
@ -81,34 +81,3 @@ elif sys.version_info >= (3, 7):
|
|||
|
||||
def contents() -> str:
|
||||
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")
|
||||
|
|
|
|||
1231
dist/ba_data/python-site-packages/typing_extensions.py
vendored
1231
dist/ba_data/python-site-packages/typing_extensions.py
vendored
File diff suppressed because it is too large
Load diff
|
|
@ -1,8 +1,13 @@
|
|||
# file generated by setuptools_scm
|
||||
# file generated by setuptools-scm
|
||||
# don't change, don't track in version control
|
||||
|
||||
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from typing import Tuple, Union
|
||||
from typing import Tuple
|
||||
from typing import Union
|
||||
|
||||
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
||||
else:
|
||||
VERSION_TUPLE = object
|
||||
|
|
@ -12,5 +17,5 @@ __version__: str
|
|||
__version_tuple__: VERSION_TUPLE
|
||||
version_tuple: VERSION_TUPLE
|
||||
|
||||
__version__ = version = '2.3.0'
|
||||
__version_tuple__ = version_tuple = (2, 3, 0)
|
||||
__version__ = version = '2.5.0'
|
||||
__version_tuple__ = version_tuple = (2, 5, 0)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
# (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]")
|
||||
|
||||
|
|
@ -232,12 +232,22 @@ class HTTPConnection(_HTTPConnection):
|
|||
super().set_tunnel(host, port=port, headers=headers)
|
||||
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:
|
||||
_MAXLINE = http.client._MAXLINE # type: ignore[attr-defined]
|
||||
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,
|
||||
)
|
||||
headers = [connect]
|
||||
|
|
@ -256,7 +266,9 @@ class HTTPConnection(_HTTPConnection):
|
|||
|
||||
if code != http.HTTPStatus.OK:
|
||||
self.close()
|
||||
raise OSError(f"Tunnel connection failed: {code} {message.strip()}")
|
||||
raise OSError(
|
||||
f"Tunnel connection failed: {code} {message.strip()}"
|
||||
)
|
||||
while True:
|
||||
line = response.fp.readline(_MAXLINE + 1)
|
||||
if len(line) > _MAXLINE:
|
||||
|
|
@ -272,6 +284,43 @@ class HTTPConnection(_HTTPConnection):
|
|||
finally:
|
||||
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:
|
||||
self.sock = self._new_conn()
|
||||
if self._tunnel_host:
|
||||
|
|
|
|||
|
|
@ -573,6 +573,11 @@ def send_jspi_request(
|
|||
"method": request.method,
|
||||
"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)
|
||||
fetcher_promise_js = js.fetch(request.url, _obj_from_dict(fetch_data))
|
||||
# Now suspend WebAssembly until we resolve that promise
|
||||
|
|
@ -693,6 +698,21 @@ def has_jspi() -> bool:
|
|||
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:
|
||||
if _fetcher:
|
||||
return _fetcher.streaming_ready
|
||||
|
|
|
|||
|
|
@ -160,14 +160,6 @@ class EmscriptenHttpResponseWrapper(BaseHTTPResponse):
|
|||
# don't cache partial content
|
||||
cache_content = False
|
||||
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)
|
||||
data = self._response.body.read()
|
||||
if cache_content:
|
||||
|
|
|
|||
|
|
@ -424,6 +424,7 @@ class PyOpenSSLContext:
|
|||
self.check_hostname = False
|
||||
self._minimum_version: int = ssl.TLSVersion.MINIMUM_SUPPORTED
|
||||
self._maximum_version: int = ssl.TLSVersion.MAXIMUM_SUPPORTED
|
||||
self._verify_flags: int = ssl.VERIFY_X509_TRUSTED_FIRST
|
||||
|
||||
@property
|
||||
def options(self) -> int:
|
||||
|
|
@ -434,6 +435,15 @@ class PyOpenSSLContext:
|
|||
self._options = value
|
||||
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
|
||||
def verify_mode(self) -> int:
|
||||
return _openssl_to_stdlib_verify[self._ctx.get_verify_mode()]
|
||||
|
|
|
|||
|
|
@ -31,11 +31,12 @@ class PoolError(HTTPError):
|
|||
|
||||
def __init__(self, pool: ConnectionPool, message: str) -> None:
|
||||
self.pool = pool
|
||||
self._message = message
|
||||
super().__init__(f"{pool}: {message}")
|
||||
|
||||
def __reduce__(self) -> _TYPE_REDUCE_RESULT:
|
||||
# For pickling purposes.
|
||||
return self.__class__, (None, None)
|
||||
return self.__class__, (None, self._message)
|
||||
|
||||
|
||||
class RequestError(PoolError):
|
||||
|
|
@ -47,7 +48,7 @@ class RequestError(PoolError):
|
|||
|
||||
def __reduce__(self) -> _TYPE_REDUCE_RESULT:
|
||||
# For pickling purposes.
|
||||
return self.__class__, (None, self.url, None)
|
||||
return self.__class__, (None, self.url, self._message)
|
||||
|
||||
|
||||
class SSLError(HTTPError):
|
||||
|
|
@ -100,6 +101,10 @@ class MaxRetryError(RequestError):
|
|||
|
||||
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):
|
||||
"""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:
|
||||
self.conn = conn
|
||||
self._message = message
|
||||
super().__init__(f"{conn}: {message}")
|
||||
|
||||
def __reduce__(self) -> _TYPE_REDUCE_RESULT:
|
||||
# For pickling purposes.
|
||||
return self.__class__, (None, None)
|
||||
return self.__class__, (None, self._message)
|
||||
|
||||
@property
|
||||
def pool(self) -> HTTPConnection:
|
||||
|
|
@ -162,11 +168,13 @@ class NameResolutionError(NewConnectionError):
|
|||
|
||||
def __init__(self, host: str, conn: HTTPConnection, reason: socket.gaierror):
|
||||
message = f"Failed to resolve '{host}' ({reason})"
|
||||
self._host = host
|
||||
self._reason = reason
|
||||
super().__init__(conn, message)
|
||||
|
||||
def __reduce__(self) -> _TYPE_REDUCE_RESULT:
|
||||
# For pickling purposes.
|
||||
return self.__class__, (None, None, None)
|
||||
return self.__class__, (self._host, None, self._reason)
|
||||
|
||||
|
||||
class EmptyPoolError(PoolError):
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ class HTTP2Connection(HTTPSConnection):
|
|||
with self._h2_conn as conn:
|
||||
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.
|
||||
header = header.encode() if isinstance(header, str) else header
|
||||
header = header.lower() # A lot of upstream code uses capitalized headers.
|
||||
|
|
|
|||
|
|
@ -203,6 +203,22 @@ class PoolManager(RequestMethods):
|
|||
**connection_pool_kw: typing.Any,
|
||||
) -> None:
|
||||
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.pools: RecentlyUsedContainer[PoolKey, HTTPConnectionPool]
|
||||
|
|
@ -456,7 +472,7 @@ class PoolManager(RequestMethods):
|
|||
kw["body"] = None
|
||||
kw["headers"] = HTTPHeaderDict(kw["headers"])._prepare_for_method_change()
|
||||
|
||||
retries = kw.get("retries")
|
||||
retries = kw.get("retries", response.retries)
|
||||
if not isinstance(retries, Retry):
|
||||
retries = Retry.from_int(retries, redirect=redirect)
|
||||
|
||||
|
|
|
|||
|
|
@ -26,23 +26,6 @@ try:
|
|||
except ImportError:
|
||||
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 ._base_connection import _TYPE_BODY
|
||||
from ._collections import HTTPHeaderDict
|
||||
|
|
@ -163,9 +146,51 @@ if brotli is not None:
|
|||
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):
|
||||
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:
|
||||
self._obj = zstd.ZstdDecompressor().decompressobj()
|
||||
|
||||
|
|
@ -183,7 +208,7 @@ if HAS_ZSTD:
|
|||
ret = self._obj.flush() # note: this is a no-op
|
||||
if not self._obj.eof:
|
||||
raise DecodeError("Zstandard data is incomplete")
|
||||
return ret
|
||||
return ret # type: ignore[no-any-return]
|
||||
|
||||
|
||||
class MultiDecoder(ContentDecoder):
|
||||
|
|
@ -518,7 +543,7 @@ class BaseHTTPResponse(io.IOBase):
|
|||
def getheaders(self) -> HTTPHeaderDict:
|
||||
warnings.warn(
|
||||
"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,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
|
@ -527,7 +552,7 @@ class BaseHTTPResponse(io.IOBase):
|
|||
def getheader(self, name: str, default: str | None = None) -> str | None:
|
||||
warnings.warn(
|
||||
"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,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
|
@ -1075,6 +1100,10 @@ class HTTPResponse(BaseHTTPResponse):
|
|||
def shutdown(self) -> None:
|
||||
if not self._sock_shutdown:
|
||||
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)
|
||||
|
||||
def close(self) -> None:
|
||||
|
|
|
|||
|
|
@ -28,12 +28,20 @@ except ImportError:
|
|||
pass
|
||||
else:
|
||||
ACCEPT_ENCODING += ",br"
|
||||
|
||||
try:
|
||||
from compression import ( # type: ignore[import-not-found] # noqa: F401
|
||||
zstd as _unused_module_zstd,
|
||||
)
|
||||
|
||||
ACCEPT_ENCODING += ",zstd"
|
||||
except ImportError:
|
||||
try:
|
||||
import zstandard as _unused_module_zstd # noqa: F401
|
||||
|
||||
ACCEPT_ENCODING += ",zstd"
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
ACCEPT_ENCODING += ",zstd"
|
||||
|
||||
|
||||
class _TYPE_FAILEDTELL(Enum):
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ try: # Do we have ssl at all?
|
|||
OPENSSL_VERSION_NUMBER,
|
||||
PROTOCOL_TLS,
|
||||
PROTOCOL_TLS_CLIENT,
|
||||
VERIFY_X509_STRICT,
|
||||
OP_NO_SSLv2,
|
||||
OP_NO_SSLv3,
|
||||
SSLContext,
|
||||
|
|
@ -109,6 +110,9 @@ try: # Do we have ssl at all?
|
|||
|
||||
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
|
||||
# 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(
|
||||
|
|
@ -117,7 +121,7 @@ try: # Do we have ssl at all?
|
|||
sys.implementation.name,
|
||||
sys.version_info,
|
||||
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
|
||||
|
||||
# Need to be careful here in case old TLS versions get
|
||||
|
|
@ -138,6 +142,8 @@ except ImportError:
|
|||
OP_NO_SSLv3 = 0x2000000 # type: ignore[assignment]
|
||||
PROTOCOL_SSLv23 = PROTOCOL_TLS = 2 # 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]
|
||||
|
|
@ -223,6 +229,7 @@ def create_urllib3_context(
|
|||
ciphers: str | None = None,
|
||||
ssl_minimum_version: int | None = None,
|
||||
ssl_maximum_version: int | None = None,
|
||||
verify_flags: int | None = None,
|
||||
) -> ssl.SSLContext:
|
||||
"""Creates and configures an :class:`ssl.SSLContext` instance for use with urllib3.
|
||||
|
||||
|
|
@ -247,6 +254,9 @@ def create_urllib3_context(
|
|||
:param ciphers:
|
||||
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.
|
||||
: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:
|
||||
Constructed SSLContext object with specified options
|
||||
:rtype: SSLContext
|
||||
|
|
@ -279,7 +289,7 @@ def create_urllib3_context(
|
|||
# keep the maximum version to be it's default value: 'TLSVersion.MAXIMUM_SUPPORTED'
|
||||
warnings.warn(
|
||||
"'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,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
|
@ -320,6 +330,16 @@ def create_urllib3_context(
|
|||
|
||||
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
|
||||
# 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
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ def match_hostname(
|
|||
if key == "commonName":
|
||||
if _dnsname_match(value, hostname):
|
||||
return
|
||||
dnsnames.append(value)
|
||||
dnsnames.append(value) # Defensive: for Python < 3.9.3
|
||||
|
||||
if len(dnsnames) > 1:
|
||||
raise CertificateError(
|
||||
|
|
|
|||
43
dist/ba_data/python/babase/__init__.py
vendored
43
dist/ba_data/python/babase/__init__.py
vendored
|
|
@ -17,8 +17,6 @@ functionality from here and reexpose it in a more focused way.
|
|||
# dependency loops. The exception is TYPE_CHECKING blocks and
|
||||
# annotations since those aren't evaluated at runtime.
|
||||
|
||||
# from efro.util import set_canonical_module_names
|
||||
|
||||
import _babase
|
||||
from _babase import (
|
||||
add_clean_frame_callback,
|
||||
|
|
@ -30,6 +28,7 @@ from _babase import (
|
|||
apptime,
|
||||
apptimer,
|
||||
AppTimer,
|
||||
atexit,
|
||||
asset_loads_allowed,
|
||||
fullscreen_control_available,
|
||||
fullscreen_control_get,
|
||||
|
|
@ -59,6 +58,7 @@ from _babase import (
|
|||
get_replays_dir,
|
||||
get_string_height,
|
||||
get_string_width,
|
||||
get_suppress_config_and_state_writes,
|
||||
get_ui_scale,
|
||||
get_v1_cloud_log_file_path,
|
||||
get_virtual_safe_area_size,
|
||||
|
|
@ -69,7 +69,7 @@ from _babase import (
|
|||
in_logic_thread,
|
||||
in_main_menu,
|
||||
increment_analytics_count,
|
||||
invoke_main_menu,
|
||||
request_main_ui,
|
||||
is_os_playing_music,
|
||||
is_xcode_build,
|
||||
lock_all_input,
|
||||
|
|
@ -79,6 +79,7 @@ from _babase import (
|
|||
mac_music_app_play_playlist,
|
||||
mac_music_app_set_volume,
|
||||
mac_music_app_stop,
|
||||
menu_press,
|
||||
music_player_play,
|
||||
music_player_set_volume,
|
||||
music_player_shutdown,
|
||||
|
|
@ -93,9 +94,9 @@ from _babase import (
|
|||
overlay_web_browser_is_supported,
|
||||
overlay_web_browser_open_url,
|
||||
print_load_info,
|
||||
push_back_press,
|
||||
pushcall,
|
||||
quit,
|
||||
reload_hooks,
|
||||
reload_media,
|
||||
request_permission,
|
||||
safecolor,
|
||||
|
|
@ -103,14 +104,15 @@ from _babase import (
|
|||
set_analytics_screen,
|
||||
set_low_level_config_value,
|
||||
set_thread_name,
|
||||
set_ui_account_state,
|
||||
set_ui_input_device,
|
||||
set_main_ui_input_device,
|
||||
set_account_sign_in_state,
|
||||
set_ui_scale,
|
||||
show_progress_bar,
|
||||
shutdown_suppress_begin,
|
||||
shutdown_suppress_end,
|
||||
shutdown_suppress_count,
|
||||
SimpleSound,
|
||||
suppress_config_and_state_writes,
|
||||
supports_max_fps,
|
||||
supports_vsync,
|
||||
supports_unicode_display,
|
||||
|
|
@ -134,7 +136,6 @@ from babase._appconfig import AppConfig
|
|||
from babase._apputils import (
|
||||
handle_leftover_v1_cloud_log_file,
|
||||
is_browser_likely_available,
|
||||
garbage_collect,
|
||||
get_remote_app_name,
|
||||
AppHealthSubsystem,
|
||||
utc_now_cloud,
|
||||
|
|
@ -145,6 +146,7 @@ from babase._devconsole import (
|
|||
DevConsoleTabEntry,
|
||||
DevConsoleSubsystem,
|
||||
)
|
||||
from babase._discord import DiscordSubsystem
|
||||
from babase._emptyappmode import EmptyAppMode
|
||||
from babase._error import (
|
||||
ContextError,
|
||||
|
|
@ -162,6 +164,7 @@ from babase._error import (
|
|||
SessionNotFoundError,
|
||||
DelegateNotFoundError,
|
||||
)
|
||||
from babase._gc import GarbageCollectionSubsystem
|
||||
from babase._general import (
|
||||
DisplayTime,
|
||||
AppTime,
|
||||
|
|
@ -176,7 +179,7 @@ from babase._general import (
|
|||
)
|
||||
from babase._language import Lstr, LanguageSubsystem
|
||||
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._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._meta import MetadataSubsystem
|
||||
from babase._net import (
|
||||
get_ip_address_type,
|
||||
DEFAULT_REQUEST_TIMEOUT_SECONDS,
|
||||
NetworkSubsystem,
|
||||
)
|
||||
from babase._env import DEFAULT_REQUEST_TIMEOUT_SECONDS
|
||||
from babase._net import get_ip_address_type, NetworkSubsystem
|
||||
from babase._plugin import PluginSpec, Plugin, PluginSubsystem
|
||||
from babase._stringedit import StringEditAdapter, StringEditSubsystem
|
||||
from babase._text import timestring
|
||||
|
|
@ -201,6 +201,7 @@ from babase._workspace import WorkspaceSubsystem
|
|||
_babase.app = app = App()
|
||||
|
||||
__all__ = [
|
||||
'accountlog',
|
||||
'AccountV2Handle',
|
||||
'AccountV2Subsystem',
|
||||
'ActivityNotFoundError',
|
||||
|
|
@ -230,6 +231,7 @@ __all__ = [
|
|||
'apptimer',
|
||||
'AppTimer',
|
||||
'asset_loads_allowed',
|
||||
'atexit',
|
||||
'balog',
|
||||
'Call',
|
||||
'fullscreen_control_available',
|
||||
|
|
@ -251,6 +253,7 @@ __all__ = [
|
|||
'DevConsoleTab',
|
||||
'DevConsoleTabEntry',
|
||||
'DevConsoleSubsystem',
|
||||
'DiscordSubsystem',
|
||||
'DisplayTime',
|
||||
'displaytime',
|
||||
'displaytimer',
|
||||
|
|
@ -263,7 +266,7 @@ __all__ = [
|
|||
'existing',
|
||||
'fade_screen',
|
||||
'fatal_error',
|
||||
'garbage_collect',
|
||||
'GarbageCollectionSubsystem',
|
||||
'get_display_resolution',
|
||||
'get_immediate_return_code',
|
||||
'get_input_idle_time',
|
||||
|
|
@ -274,6 +277,7 @@ __all__ = [
|
|||
'get_replays_dir',
|
||||
'get_string_height',
|
||||
'get_string_width',
|
||||
'get_suppress_config_and_state_writes',
|
||||
'get_type_name',
|
||||
'get_ui_scale',
|
||||
'get_virtual_safe_area_size',
|
||||
|
|
@ -289,7 +293,7 @@ __all__ = [
|
|||
'increment_analytics_count',
|
||||
'InputDeviceNotFoundError',
|
||||
'InputType',
|
||||
'invoke_main_menu',
|
||||
'request_main_ui',
|
||||
'is_browser_likely_available',
|
||||
'is_browser_likely_available',
|
||||
'is_os_playing_music',
|
||||
|
|
@ -309,6 +313,7 @@ __all__ = [
|
|||
'mac_music_app_set_volume',
|
||||
'mac_music_app_stop',
|
||||
'MapNotFoundError',
|
||||
'menu_press',
|
||||
'MetadataSubsystem',
|
||||
'music_player_play',
|
||||
'music_player_set_volume',
|
||||
|
|
@ -317,6 +322,7 @@ __all__ = [
|
|||
'native_review_request',
|
||||
'native_review_request_supported',
|
||||
'native_stack_trace',
|
||||
'netlog',
|
||||
'NetworkSubsystem',
|
||||
'NodeNotFoundError',
|
||||
'normalized_color',
|
||||
|
|
@ -333,10 +339,10 @@ __all__ = [
|
|||
'PluginSubsystem',
|
||||
'PluginSpec',
|
||||
'print_load_info',
|
||||
'push_back_press',
|
||||
'pushcall',
|
||||
'quit',
|
||||
'QuitType',
|
||||
'reload_hooks',
|
||||
'reload_media',
|
||||
'request_permission',
|
||||
'safecolor',
|
||||
|
|
@ -346,15 +352,16 @@ __all__ = [
|
|||
'SessionTeamNotFoundError',
|
||||
'set_analytics_screen',
|
||||
'set_low_level_config_value',
|
||||
'set_main_ui_input_device',
|
||||
'set_thread_name',
|
||||
'set_ui_account_state',
|
||||
'set_ui_input_device',
|
||||
'set_account_sign_in_state',
|
||||
'set_ui_scale',
|
||||
'show_progress_bar',
|
||||
'shutdown_suppress_begin',
|
||||
'shutdown_suppress_end',
|
||||
'shutdown_suppress_count',
|
||||
'SimpleSound',
|
||||
'suppress_config_and_state_writes',
|
||||
'SpecialChar',
|
||||
'storagename',
|
||||
'StringEditAdapter',
|
||||
|
|
|
|||
34
dist/ba_data/python/babase/_accountv2.py
vendored
34
dist/ba_data/python/babase/_accountv2.py
vendored
|
|
@ -12,6 +12,8 @@ from typing import TYPE_CHECKING, assert_never
|
|||
from efro.error import CommunicationError
|
||||
from efro.call import CallbackSet
|
||||
from bacommon.login import LoginType
|
||||
|
||||
from babase._logging import accountlog
|
||||
import _babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -19,8 +21,6 @@ if TYPE_CHECKING:
|
|||
|
||||
from babase._login import LoginAdapter, LoginInfo
|
||||
|
||||
logger = logging.getLogger('ba.accountv2')
|
||||
|
||||
|
||||
class AccountV2Subsystem:
|
||||
"""Subsystem for modern account handling in the app.
|
||||
|
|
@ -104,6 +104,12 @@ class AccountV2Subsystem:
|
|||
"""
|
||||
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.
|
||||
for call in self.on_primary_account_changed_callbacks.getcalls():
|
||||
try:
|
||||
|
|
@ -280,7 +286,7 @@ class AccountV2Subsystem:
|
|||
# generally this means the user has explicitly signed in/out or
|
||||
# switched accounts within that back-end.
|
||||
if prev_state != new_state:
|
||||
logger.debug(
|
||||
accountlog.debug(
|
||||
'Implicit state changed (%s -> %s);'
|
||||
' will update app sign-in state accordingly.',
|
||||
prev_state,
|
||||
|
|
@ -324,7 +330,7 @@ class AccountV2Subsystem:
|
|||
if self._implicit_signed_in_adapter is None:
|
||||
# If implicit back-end has signed out, we follow suit
|
||||
# immediately; no need to wait for network connectivity.
|
||||
logger.debug(
|
||||
accountlog.debug(
|
||||
'Signing out as result of implicit state change...',
|
||||
)
|
||||
plus.accounts.set_primary_credentials(None)
|
||||
|
|
@ -343,7 +349,7 @@ class AccountV2Subsystem:
|
|||
# switching accounts via the back-end). NOTE: should
|
||||
# test case where we don't have connectivity here.
|
||||
if plus.cloud.is_connected():
|
||||
logger.debug(
|
||||
accountlog.debug(
|
||||
'Signing in as result of implicit state change...',
|
||||
)
|
||||
self._implicit_signed_in_adapter.sign_in(
|
||||
|
|
@ -352,15 +358,15 @@ class AccountV2Subsystem:
|
|||
)
|
||||
self._implicit_state_changed = False
|
||||
|
||||
# Once we've made a move here we don't want to
|
||||
# do any more automatic stuff.
|
||||
# Once we've made a move here we don't want to do
|
||||
# any more automatic stuff.
|
||||
self._can_do_auto_sign_in = False
|
||||
|
||||
if not self._can_do_auto_sign_in:
|
||||
return
|
||||
|
||||
# If we're not currently signed in, we have connectivity, and
|
||||
# we have an available implicit login, auto-sign-in with it once.
|
||||
# If we're not currently signed in, we have connectivity, and we
|
||||
# have an available implicit login, auto-sign-in with it once.
|
||||
# The implicit-state-change logic above should keep things
|
||||
# mostly in-sync, but that might not always be the case due to
|
||||
# connectivity or other issues. We prefer to keep people signed
|
||||
|
|
@ -376,7 +382,7 @@ class AccountV2Subsystem:
|
|||
and not signed_in_v2
|
||||
and self._implicit_signed_in_adapter is not None
|
||||
):
|
||||
logger.debug(
|
||||
accountlog.debug(
|
||||
'Signing in due to on-launch-auto-sign-in...',
|
||||
)
|
||||
self._can_do_auto_sign_in = False # Only ATTEMPT once
|
||||
|
|
@ -397,11 +403,11 @@ class AccountV2Subsystem:
|
|||
plus = _babase.app.plus
|
||||
assert plus is not None
|
||||
|
||||
# Make some noise on errors since the user knows a
|
||||
# sign-in attempt is happening in this case (the 'explicit' part).
|
||||
# Make some noise on errors since the user knows a sign-in
|
||||
# attempt is happening in this case (the 'explicit' part).
|
||||
if isinstance(result, Exception):
|
||||
# We expect the occasional communication errors;
|
||||
# Log a full exception for anything else though.
|
||||
# We expect the occasional communication errors; Log a full
|
||||
# exception for anything else though.
|
||||
if not isinstance(result, CommunicationError):
|
||||
logging.warning(
|
||||
'Error on explicit accountv2 sign in attempt.',
|
||||
|
|
|
|||
142
dist/ba_data/python/babase/_app.py
vendored
142
dist/ba_data/python/babase/_app.py
vendored
|
|
@ -5,6 +5,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
from enum import Enum
|
||||
from functools import partial
|
||||
|
|
@ -12,8 +13,10 @@ from typing import TYPE_CHECKING, override
|
|||
from threading import RLock
|
||||
|
||||
from efro.threadpool import ThreadPoolExecutorEx
|
||||
from efro.util import strip_exception_tracebacks
|
||||
|
||||
import _babase
|
||||
from babase._discord import DiscordSubsystem
|
||||
from babase._language import LanguageSubsystem
|
||||
from babase._locale import LocaleSubsystem
|
||||
from babase._plugin import PluginSubsystem
|
||||
|
|
@ -27,6 +30,7 @@ from babase._stringedit import StringEditSubsystem
|
|||
from babase._devconsole import DevConsoleSubsystem
|
||||
from babase._appconfig import AppConfig
|
||||
from babase._logging import lifecyclelog, applog
|
||||
from babase._gc import GarbageCollectionSubsystem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import asyncio
|
||||
|
|
@ -64,6 +68,9 @@ class App:
|
|||
#: Subsystem for keeping tabs on app health.
|
||||
health: AppHealthSubsystem
|
||||
|
||||
#: Subsystem for network functionality.
|
||||
net: NetworkSubsystem
|
||||
|
||||
#: How long we allow shutdown tasks to run before killing them.
|
||||
#: Currently the entire app hard-exits if shutdown takes 15 seconds,
|
||||
#: 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,
|
||||
)
|
||||
|
||||
#: Garbage collection related functionality.
|
||||
self.gc: GarbageCollectionSubsystem = self.register_subsystem(
|
||||
GarbageCollectionSubsystem()
|
||||
)
|
||||
|
||||
#: Locale related functionality.
|
||||
self.locale: LocaleSubsystem = self.register_subsystem(
|
||||
LocaleSubsystem()
|
||||
|
|
@ -120,16 +132,18 @@ class App:
|
|||
PluginSubsystem()
|
||||
)
|
||||
|
||||
#: Subsystem for discord functionality
|
||||
self.discord: DiscordSubsystem = self.register_subsystem(
|
||||
DiscordSubsystem()
|
||||
)
|
||||
|
||||
#: Subsystem for wrangling metadata.
|
||||
self.meta: MetadataSubsystem = MetadataSubsystem()
|
||||
|
||||
#: Subsystem for network functionality.
|
||||
self.net: NetworkSubsystem = NetworkSubsystem()
|
||||
|
||||
#: Subsystem for wrangling workspaces.
|
||||
self.workspaces: WorkspaceSubsystem = WorkspaceSubsystem()
|
||||
|
||||
# (not actually in use yet)
|
||||
#: :meta private:
|
||||
self.components: AppComponentSubsystem = AppComponentSubsystem()
|
||||
|
||||
#: Subsystem for wrangling text input from various sources.
|
||||
|
|
@ -242,19 +256,6 @@ class App:
|
|||
self._asyncio_tasks.add(task)
|
||||
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
|
||||
def mode_selector(self) -> babase.AppModeSelector:
|
||||
"""Controls which app-modes are used for handling given intents.
|
||||
|
|
@ -274,6 +275,24 @@ class App:
|
|||
def mode_selector(self, selector: babase.AppModeSelector) -> None:
|
||||
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(
|
||||
self, ssname: str, create_call: Callable[[], AppSubsystem | None]
|
||||
) -> AppSubsystem | None:
|
||||
|
|
@ -409,9 +428,16 @@ class App:
|
|||
def add_shutdown_task(self, coro: Coroutine[None, None, None]) -> None:
|
||||
"""Add a task to be run on app shutdown.
|
||||
|
||||
Note that shutdown tasks will be canceled after
|
||||
:py:const:`SHUTDOWN_TASK_TIMEOUT_SECONDS` if they are still
|
||||
running.
|
||||
All shutdown tasks will be run concurrently alongside a fade-out,
|
||||
so it is ok for them to take a moment or two to do their thing.
|
||||
|
||||
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 (
|
||||
self.state is AppState.SHUTTING_DOWN
|
||||
|
|
@ -423,6 +449,36 @@ class App:
|
|||
)
|
||||
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:
|
||||
"""Run the app to completion.
|
||||
|
||||
|
|
@ -746,8 +802,18 @@ class App:
|
|||
|
||||
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()
|
||||
|
||||
self.net = NetworkSubsystem()
|
||||
self._asyncio_loop = _asyncio.setup_asyncio()
|
||||
self.health = self.register_subsystem(AppHealthSubsystem())
|
||||
|
||||
|
|
@ -870,15 +936,15 @@ class App:
|
|||
# be added at this point.
|
||||
for subsystem in self._subsystems.copy():
|
||||
try:
|
||||
subsystem.do_apply_app_config()
|
||||
subsystem.apply_app_config()
|
||||
except Exception:
|
||||
logging.exception(
|
||||
'Error in do_apply_app_config() for subsystem %s.',
|
||||
'Error in apply_app_config() for subsystem %s.',
|
||||
subsystem,
|
||||
)
|
||||
|
||||
# Let the native layer do its thing.
|
||||
_babase.do_apply_app_config()
|
||||
_babase.apply_app_config()
|
||||
|
||||
def _update_state(self) -> None:
|
||||
# pylint: disable=too-many-branches
|
||||
|
|
@ -906,6 +972,7 @@ class App:
|
|||
# Entering suspended state:
|
||||
if self.state is not AppState.SUSPENDED:
|
||||
self.state = AppState.SUSPENDED
|
||||
lifecyclelog.info('app-state is now %s', self.state.name)
|
||||
self._on_suspend()
|
||||
else:
|
||||
# Leaving suspended state:
|
||||
|
|
@ -1084,11 +1151,32 @@ class App:
|
|||
|
||||
# Kick off a short fade and give it time to complete.
|
||||
lifecyclelog.info('fade-and-shutdown-graphics begin')
|
||||
_babase.fade_screen(False, time=0.15)
|
||||
await asyncio.sleep(0.15)
|
||||
fade_done = False
|
||||
|
||||
# Now tell the graphics system to go down and wait until
|
||||
# it has done so.
|
||||
starttime = time.monotonic()
|
||||
|
||||
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()
|
||||
while not _babase.graphics_shutdown_is_complete():
|
||||
await asyncio.sleep(0.01)
|
||||
|
|
|
|||
4
dist/ba_data/python/babase/_appcomponent.py
vendored
4
dist/ba_data/python/babase/_appcomponent.py
vendored
|
|
@ -30,6 +30,10 @@ class AppComponentSubsystem:
|
|||
Change-callbacks can also be requested for base classes which will
|
||||
fire in a deferred manner when particular base-classes are
|
||||
overridden.
|
||||
|
||||
(This isn't ready for use yet so hiding it from docs)
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
|
|
|||
19
dist/ba_data/python/babase/_appconfig.py
vendored
19
dist/ba_data/python/babase/_appconfig.py
vendored
|
|
@ -49,15 +49,16 @@ class AppConfig(dict):
|
|||
def default_value(self, key: str) -> Any:
|
||||
"""Given a string key, return its predefined default value.
|
||||
|
||||
This is the value that will be returned by :meth:`resolve()`
|
||||
if the key is not present in the config dict or of an incompatible
|
||||
This is the value that will be returned by :meth:`resolve()` if
|
||||
the key is not present in the config dict or of an incompatible
|
||||
type.
|
||||
|
||||
Raises an Exception for unrecognized key names. To get the list of keys
|
||||
supported by this method, use babase.AppConfig.builtin_keys(). Note
|
||||
that it is perfectly legal to store other data in the config; it just
|
||||
needs to be accessed through standard dict methods and missing values
|
||||
handled manually.
|
||||
Raises an Exception for unrecognized key names. To get the list
|
||||
of keys supported by this method, use
|
||||
babase.AppConfig.builtin_keys(). Note that it is perfectly legal
|
||||
to store other data in the config; it just needs to be accessed
|
||||
through standard dict methods and missing values handled
|
||||
manually.
|
||||
"""
|
||||
return _babase.get_appconfig_default_value(key)
|
||||
|
||||
|
|
@ -89,8 +90,8 @@ class AppConfig(dict):
|
|||
def commit(self) -> None:
|
||||
"""Commits the config to local storage.
|
||||
|
||||
Note that this call is asynchronous so the actual write to disk may not
|
||||
occur immediately.
|
||||
Note that this call is asynchronous so the actual write to disk
|
||||
may not occur immediately.
|
||||
"""
|
||||
commit_app_config()
|
||||
|
||||
|
|
|
|||
2
dist/ba_data/python/babase/_appsubsystem.py
vendored
2
dist/ba_data/python/babase/_appsubsystem.py
vendored
|
|
@ -56,7 +56,7 @@ class AppSubsystem:
|
|||
: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."""
|
||||
|
||||
def on_ui_scale_change(self) -> None:
|
||||
|
|
|
|||
107
dist/ba_data/python/babase/_apputils.py
vendored
107
dist/ba_data/python/babase/_apputils.py
vendored
|
|
@ -3,9 +3,11 @@
|
|||
"""Utility functionality related to the overall operation of the app."""
|
||||
from __future__ import annotations
|
||||
|
||||
import gc
|
||||
import os
|
||||
from threading import Thread
|
||||
import json
|
||||
import time
|
||||
import asyncio
|
||||
import threading
|
||||
from functools import partial
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
|
@ -140,6 +142,12 @@ def handle_v1_cloud_log() -> None:
|
|||
|
||||
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
|
||||
|
||||
# Delay our log upload slightly in case other pertinent info
|
||||
|
|
@ -164,14 +172,32 @@ def handle_leftover_v1_cloud_log_file() -> None:
|
|||
|
||||
: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.
|
||||
if _babase.app.classic is None:
|
||||
if _babase.app.classic is None or _babase.app.plus is None:
|
||||
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(
|
||||
_babase.get_v1_cloud_log_file_path(), encoding='utf-8'
|
||||
) as infile:
|
||||
|
|
@ -192,50 +218,15 @@ def handle_leftover_v1_cloud_log_file() -> None:
|
|||
# killed it since. ¯\_(ツ)_/¯
|
||||
pass
|
||||
|
||||
_babase.app.classic.master_server_v1_post(
|
||||
'bsLog', info, response
|
||||
)
|
||||
_babase.app.classic.master_server_v1_post('bsLog', info, response)
|
||||
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())
|
||||
|
||||
except Exception:
|
||||
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:
|
||||
"""Print an error if a corrupt file is found."""
|
||||
|
||||
|
|
@ -401,11 +392,23 @@ class AppHealthSubsystem(AppSubsystem):
|
|||
assert _babase.in_logic_thread()
|
||||
super().__init__()
|
||||
self._running = True
|
||||
self._thread = Thread(target=self._app_monitor_thread_main, daemon=True)
|
||||
self._thread.start()
|
||||
self._response = False
|
||||
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
|
||||
def on_app_loading(self) -> None:
|
||||
""":meta private:"""
|
||||
|
|
@ -441,15 +444,15 @@ class AppHealthSubsystem(AppSubsystem):
|
|||
return self._running
|
||||
|
||||
def _monitor_app(self) -> None:
|
||||
import time
|
||||
|
||||
while bool(True):
|
||||
# Always sleep a bit between checks.
|
||||
time.sleep(1.234)
|
||||
while not self.stop_event.is_set():
|
||||
|
||||
# # Always sleep a bit between checks.
|
||||
self.stop_event.wait(1.234)
|
||||
|
||||
# Do nothing while backgrounded.
|
||||
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.
|
||||
starttime = time.monotonic()
|
||||
|
|
@ -479,6 +482,8 @@ class AppHealthSubsystem(AppSubsystem):
|
|||
# We just do one alert for now.
|
||||
return
|
||||
|
||||
time.sleep(1.042)
|
||||
self.stop_event.wait(1.042)
|
||||
|
||||
self._first_check = False
|
||||
|
||||
self.stopped_event.set()
|
||||
|
|
|
|||
27
dist/ba_data/python/babase/_assetmanager.py
vendored
27
dist/ba_data/python/babase/_assetmanager.py
vendored
|
|
@ -8,7 +8,6 @@ from typing import TYPE_CHECKING, Annotated
|
|||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
import threading
|
||||
import urllib.request
|
||||
import logging
|
||||
import weakref
|
||||
import time
|
||||
|
|
@ -21,9 +20,10 @@ from efro.dataclassio import (
|
|||
dataclass_from_json,
|
||||
dataclass_to_json,
|
||||
)
|
||||
import _babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
from bacommon.assets import AssetPackageFlavor
|
||||
|
||||
|
||||
|
|
@ -174,23 +174,28 @@ class AssetGather:
|
|||
|
||||
def fetch_url(url: str, filename: Path, asset_gather: AssetGather) -> None:
|
||||
"""Fetch a given url to a given filename for a given AssetGather."""
|
||||
# pylint: disable=consider-using-with
|
||||
import socket
|
||||
|
||||
del url # Unused.
|
||||
|
||||
# We don't want to keep the provided AssetGather alive, but we want
|
||||
# to abort if it dies.
|
||||
assert isinstance(asset_gather, AssetGather)
|
||||
# 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
|
||||
# to cancel even with network blockage.
|
||||
req = urllib.request.urlopen(
|
||||
urllib.request.Request(
|
||||
url, None, {'User-Agent': _babase.user_agent_string()}
|
||||
),
|
||||
context=_babase.app.net.sslcontext,
|
||||
timeout=1,
|
||||
)
|
||||
req: Any = None
|
||||
# req = urllib.request.urlopen(
|
||||
# urllib.request.Request(
|
||||
# url, None, {'User-Agent': _babase.user_agent_string()}
|
||||
# ),
|
||||
# context=_babase.app.net.sslcontext,
|
||||
# timeout=1,
|
||||
# )
|
||||
file_size = int(req.headers['Content-Length'])
|
||||
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.fp.close()
|
||||
|
||||
threading.Thread(target=doit).run()
|
||||
threading.Thread(target=doit).start()
|
||||
|
||||
with open(filename, 'wb') as outfile:
|
||||
file_size_dl = 0
|
||||
|
|
|
|||
19
dist/ba_data/python/babase/_asyncio.py
vendored
19
dist/ba_data/python/babase/_asyncio.py
vendored
|
|
@ -15,7 +15,11 @@ import logging
|
|||
import time
|
||||
import os
|
||||
|
||||
from efro.util import strip_exception_tracebacks
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
import babase
|
||||
|
||||
# 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.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
|
||||
# low level event loop so that asyncio timers/sockets/etc. could
|
||||
# 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())
|
||||
|
||||
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)
|
||||
|
|
|
|||
11
dist/ba_data/python/babase/_devconsole.py
vendored
11
dist/ba_data/python/babase/_devconsole.py
vendored
|
|
@ -80,11 +80,12 @@ class DevConsoleTab:
|
|||
h_align: Literal['left', 'center', 'right'] = 'center',
|
||||
v_align: Literal['top', 'center', 'bottom', 'none'] = 'center',
|
||||
scale: float = 1.0,
|
||||
style: Literal['normal', 'faded'] = 'normal',
|
||||
) -> None:
|
||||
"""Add a button to the tab being refreshed."""
|
||||
assert _babase.app.devconsole.is_refreshing
|
||||
_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:
|
||||
|
|
@ -154,13 +155,19 @@ class DevConsoleSubsystem:
|
|||
DevConsoleTabEntry('Python', DevConsoleTabPython),
|
||||
DevConsoleTabEntry('AppModes', DevConsoleTabAppModes),
|
||||
DevConsoleTabEntry('UI', DevConsoleTabUI),
|
||||
DevConsoleTabEntry('Logging', DevConsoleTabLogging),
|
||||
DevConsoleTabEntry('LogLevels', DevConsoleTabLogging),
|
||||
]
|
||||
if os.environ.get('BA_DEV_CONSOLE_TEST_TAB', '0') == '1':
|
||||
self.tabs.append(DevConsoleTabEntry('Test', DevConsoleTabTest))
|
||||
self.is_refreshing = False
|
||||
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:
|
||||
"""Called by the C++ layer when a tab should be filled out.
|
||||
|
||||
|
|
|
|||
77
dist/ba_data/python/babase/_devconsoletabs.py
vendored
77
dist/ba_data/python/babase/_devconsoletabs.py
vendored
|
|
@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, override
|
|||
|
||||
import _babase
|
||||
|
||||
from babase._logging import description_for_logger
|
||||
from babase._devconsole import DevConsoleTab
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -72,7 +73,7 @@ class DevConsoleTabAppModes(DevConsoleTab):
|
|||
self.text(
|
||||
'Available AppModes:',
|
||||
scale=0.8,
|
||||
pos=(0, 75),
|
||||
pos=(0, 78),
|
||||
h_align='center',
|
||||
v_align='center',
|
||||
)
|
||||
|
|
@ -80,7 +81,7 @@ class DevConsoleTabAppModes(DevConsoleTab):
|
|||
for i, mode in enumerate(self._app_modes):
|
||||
self.button(
|
||||
f'{mode.__module__}.{mode.__qualname__}',
|
||||
pos=(xoffs + i * bwidth + bpadding, 10),
|
||||
pos=(xoffs + i * bwidth + bpadding, 15),
|
||||
size=(bwidth - 2.0 * bpadding, 40),
|
||||
label_scale=0.6,
|
||||
call=partial(self._set_app_mode, mode),
|
||||
|
|
@ -118,13 +119,14 @@ class DevConsoleTabUI(DevConsoleTab):
|
|||
def refresh(self) -> None:
|
||||
from babase._mgen.enums import UIScale
|
||||
|
||||
xoffs = -375
|
||||
xoffs = -305
|
||||
yoffs = 10
|
||||
|
||||
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.',
|
||||
scale=0.6,
|
||||
pos=(xoffs + 15, 70),
|
||||
pos=(xoffs + 15, yoffs + 65),
|
||||
h_align='left',
|
||||
v_align='center',
|
||||
)
|
||||
|
|
@ -132,7 +134,7 @@ class DevConsoleTabUI(DevConsoleTab):
|
|||
ui_overlay = _babase.get_draw_virtual_safe_area_bounds()
|
||||
self.button(
|
||||
'Virtual Safe Area ON' if ui_overlay else 'Virtual Safe Area OFF',
|
||||
pos=(xoffs + 10, 10),
|
||||
pos=(xoffs + 10, yoffs + 10),
|
||||
size=(200, 30),
|
||||
label_scale=0.6,
|
||||
call=self.toggle_ui_overlay,
|
||||
|
|
@ -141,7 +143,7 @@ class DevConsoleTabUI(DevConsoleTab):
|
|||
x = 300
|
||||
self.text(
|
||||
'UI-Scale',
|
||||
pos=(xoffs + x - 5, 15),
|
||||
pos=(xoffs + x - 5, yoffs + 15),
|
||||
h_align='right',
|
||||
v_align='none',
|
||||
scale=0.6,
|
||||
|
|
@ -151,7 +153,7 @@ class DevConsoleTabUI(DevConsoleTab):
|
|||
for scale in UIScale:
|
||||
self.button(
|
||||
scale.name.capitalize(),
|
||||
pos=(xoffs + x, 10),
|
||||
pos=(xoffs + x, yoffs + 10),
|
||||
size=(bwidth, 30),
|
||||
label_scale=0.6,
|
||||
call=partial(_babase.app.set_ui_scale, scale),
|
||||
|
|
@ -187,6 +189,7 @@ class Table[T]:
|
|||
margin_left_right: float = 60.0,
|
||||
debug_bounds: bool = False,
|
||||
max_columns: int | None = None,
|
||||
focus_entry_config_key: str | None = None,
|
||||
) -> None:
|
||||
self._title = title
|
||||
self._entry_width = entry_width
|
||||
|
|
@ -198,12 +201,19 @@ class Table[T]:
|
|||
self._entries = entries
|
||||
self._draw_entry_call = draw_entry_call
|
||||
self._max_columns = max_columns
|
||||
self._focus_entry_config_key = focus_entry_config_key
|
||||
|
||||
# Values updated on refresh (for aligning other custom
|
||||
# widgets/etc.)
|
||||
self.top_left: 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:
|
||||
"""Update table entries."""
|
||||
self._entries = entries
|
||||
|
|
@ -219,6 +229,11 @@ class Table[T]:
|
|||
This affects which page is shown at the next refresh.
|
||||
"""
|
||||
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:
|
||||
"""Call to refresh the data."""
|
||||
|
|
@ -386,21 +401,28 @@ class DevConsoleTabLogging(DevConsoleTab):
|
|||
self._table = Table(
|
||||
title='Logging Levels',
|
||||
entry_width=800,
|
||||
entry_height=42,
|
||||
entry_height=44,
|
||||
debug_bounds=False,
|
||||
entries=list[str](),
|
||||
draw_entry_call=self._draw_entry,
|
||||
max_columns=1,
|
||||
focus_entry_config_key='Logging Levels Focus Entry',
|
||||
)
|
||||
|
||||
@override
|
||||
def refresh(self) -> None:
|
||||
|
||||
assert self._table is not None
|
||||
|
||||
# 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(
|
||||
['root'] + sorted(logging.root.manager.loggerDict)
|
||||
['root']
|
||||
+ sorted(
|
||||
logging.root.manager.loggerDict,
|
||||
key=lambda name: (name.split('.')[0] != 'ba', name),
|
||||
)
|
||||
)
|
||||
|
||||
# Draw the table.
|
||||
|
|
@ -490,13 +512,30 @@ class DevConsoleTabLogging(DevConsoleTab):
|
|||
xoffs = -15.0
|
||||
bwidth = 80.0
|
||||
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(
|
||||
entry,
|
||||
(
|
||||
x + width - bwidth * 6.5 - 10.0 + xoffs,
|
||||
y + height * 0.5,
|
||||
# x + width - bwidth * 6.5 - 10.0 + xoffs,
|
||||
x + 12,
|
||||
y + height * 0.5 + yoffs,
|
||||
),
|
||||
h_align='right',
|
||||
h_align='left',
|
||||
scale=0.7,
|
||||
)
|
||||
|
||||
|
|
@ -504,14 +543,6 @@ class DevConsoleTabLogging(DevConsoleTab):
|
|||
level = logger.level
|
||||
index = 0
|
||||
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'
|
||||
tab.button(
|
||||
notsetname,
|
||||
|
|
@ -534,7 +565,6 @@ class DevConsoleTabLogging(DevConsoleTab):
|
|||
if level == logging.DEBUG
|
||||
else 'blue' if effectivelevel <= logging.DEBUG else 'black'
|
||||
),
|
||||
# style='bright' if level == logging.DEBUG else 'normal',
|
||||
call=partial(
|
||||
self._set_entry_val, entry_index, entry, logging.DEBUG
|
||||
),
|
||||
|
|
@ -550,7 +580,6 @@ class DevConsoleTabLogging(DevConsoleTab):
|
|||
if level == logging.INFO
|
||||
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),
|
||||
)
|
||||
index += 1
|
||||
|
|
|
|||
121
dist/ba_data/python/babase/_discord.py
vendored
Normal file
121
dist/ba_data/python/babase/_discord.py
vendored
Normal 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)
|
||||
434
dist/ba_data/python/babase/_env.py
vendored
434
dist/ba_data/python/babase/_env.py
vendored
|
|
@ -3,20 +3,34 @@
|
|||
"""Environment related functionality."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import ssl
|
||||
import time
|
||||
import signal
|
||||
import logging
|
||||
import warnings
|
||||
import threading
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
import urllib3
|
||||
from efro.logging import LogLevel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
from efro.logging import LogEntry, LogHandler
|
||||
|
||||
_g_babase_imported = False # pylint: disable=invalid-name
|
||||
_g_babase_app_started = False # pylint: disable=invalid-name
|
||||
# Timeout for standard functions talking to the master-server/etc. We
|
||||
# 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:
|
||||
|
|
@ -26,6 +40,7 @@ def on_native_module_import() -> None:
|
|||
environment modifications until we actually commit to running an
|
||||
app.
|
||||
"""
|
||||
# pylint: disable=cyclic-import
|
||||
import _babase
|
||||
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
|
||||
# output.
|
||||
envconfig = baenv.get_config()
|
||||
envconfig = baenv.get_env_config()
|
||||
if envconfig.log_handler is not None:
|
||||
_feed_logs_to_babase(envconfig.log_handler)
|
||||
|
||||
|
|
@ -45,22 +60,24 @@ def on_native_module_import() -> None:
|
|||
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
|
||||
# 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:
|
||||
logging.warning(
|
||||
logging.error(
|
||||
'These scripts are meant to be used with'
|
||||
' 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,
|
||||
running_build,
|
||||
__file__,
|
||||
)
|
||||
|
||||
debug_build = env['debug_build']
|
||||
debug_build = pre_env['debug_build']
|
||||
|
||||
# We expect dev_mode on in debug builds and off otherwise;
|
||||
# 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
|
||||
signal-handling, garbage-collection, and logging.
|
||||
"""
|
||||
# pylint: disable=cyclic-import
|
||||
import gc
|
||||
import baenv
|
||||
import _babase
|
||||
|
|
@ -91,7 +109,7 @@ def on_main_thread_start_app() -> None:
|
|||
_g_babase_app_started = True
|
||||
|
||||
assert _g_babase_imported
|
||||
assert baenv.config_exists()
|
||||
assert baenv.env_config_exists()
|
||||
|
||||
# If we were unable to set paths earlier, complain now.
|
||||
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.
|
||||
warnings.simplefilter('default', DeprecationWarning)
|
||||
|
||||
# Turn off fancy-pants cyclic garbage-collection. We run it only at
|
||||
# explicit times to avoid random hitches and keep things more
|
||||
# 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).
|
||||
# Set up our garbage collection stuff.
|
||||
_babase.app.gc.set_initial_mode()
|
||||
|
||||
# FIXME - move this to Python bootstrapping code. or perhaps disable
|
||||
# it completely since we've got more bg stuff happening now?...
|
||||
# (but put safeguards in place to time/minimize gc pauses).
|
||||
gc.disable()
|
||||
if os.environ.get('BA_GC_DEBUG_LEAK') == '1':
|
||||
print('ENABLING GC DEBUG LEAK CHECKS', file=sys.stderr)
|
||||
gc.set_debug(gc.DEBUG_LEAK)
|
||||
|
||||
# pylint: disable=c-extension-no-member
|
||||
if not TYPE_CHECKING:
|
||||
import __main__
|
||||
|
||||
# Clear out the standard quit/exit messages since they don't
|
||||
# work in our embedded situation (should revisit this once we're
|
||||
# usable from a standard interpreter). Note that these don't
|
||||
# work in our embedded situations and we wouldn't want to use them
|
||||
# if they did since Note that these don't
|
||||
# exist in the first place for our monolithic builds which don't
|
||||
# use site.py.
|
||||
for attr in ('quit', 'exit'):
|
||||
|
|
@ -164,6 +178,334 @@ def on_main_thread_start_app() -> None:
|
|||
|
||||
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:
|
||||
"""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
|
||||
# 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:
|
||||
_babase.screenmessage(
|
||||
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:
|
||||
"""Route log/print output to internal ballistica console/etc."""
|
||||
import _babase
|
||||
|
|
|
|||
564
dist/ba_data/python/babase/_gc.py
vendored
Normal file
564
dist/ba_data/python/babase/_gc.py
vendored
Normal 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
|
||||
14
dist/ba_data/python/babase/_general.py
vendored
14
dist/ba_data/python/babase/_general.py
vendored
|
|
@ -34,7 +34,11 @@ DisplayTime = NewType('DisplayTime', float)
|
|||
|
||||
|
||||
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:
|
||||
"""Whether this object exists."""
|
||||
|
|
@ -43,17 +47,17 @@ class Existable(Protocol):
|
|||
def existing[ExistableT: Existable](
|
||||
obj: 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
|
||||
references not be passed around and instead get converted to values
|
||||
of None. That way the type checker can properly flag attempts to
|
||||
pass possibly-dead objects (``FooType | None``) into functions
|
||||
expecting only live ones (``FooType``), etc. This call can be used
|
||||
on any 'existable' object (one with an ``exists()`` method) and will
|
||||
convert it to a ``None`` value if it does not exist.
|
||||
on any 'existable' object (one with an ``exists()`` method) to
|
||||
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
|
||||
"""
|
||||
assert obj is None or hasattr(obj, 'exists'), f'No "exists" attr on {obj}.'
|
||||
|
|
|
|||
18
dist/ba_data/python/babase/_hooks.py
vendored
18
dist/ba_data/python/babase/_hooks.py
vendored
|
|
@ -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:
|
||||
from babase._language import Lstr
|
||||
|
||||
|
|
@ -251,9 +245,9 @@ def unavailable_message() -> None:
|
|||
|
||||
|
||||
def set_last_ad_network(sval: str) -> None:
|
||||
if _babase.app.classic is not None:
|
||||
_babase.app.classic.ads.last_ad_network = sval
|
||||
_babase.app.classic.ads.last_ad_network_set_time = time.time()
|
||||
if _babase.app.plus is not None:
|
||||
_babase.app.plus.ads.last_ad_network = sval
|
||||
_babase.app.plus.ads.last_ad_network_set_time = time.time()
|
||||
|
||||
|
||||
def google_play_purchases_not_available_message() -> None:
|
||||
|
|
@ -305,8 +299,8 @@ def ui_remote_press() -> None:
|
|||
|
||||
|
||||
def remove_in_game_ads_message() -> None:
|
||||
if _babase.app.classic is not None:
|
||||
_babase.app.classic.ads.do_remove_in_game_ads_message()
|
||||
if _babase.app.plus is not None:
|
||||
_babase.app.plus.ads.do_remove_in_game_ads_message()
|
||||
|
||||
|
||||
def do_quit() -> None:
|
||||
|
|
@ -446,7 +440,7 @@ def copy_dev_console_history() -> None:
|
|||
return
|
||||
|
||||
# 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:
|
||||
_babase.getsimplesound('error').play()
|
||||
_babase.screenmessage(
|
||||
|
|
|
|||
6
dist/ba_data/python/babase/_language.py
vendored
6
dist/ba_data/python/babase/_language.py
vendored
|
|
@ -96,6 +96,12 @@ class LanguageSubsystem(AppSubsystem):
|
|||
def _update_test_language(self, langid: str) -> None:
|
||||
if _babase.app.classic is None:
|
||||
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(
|
||||
'bsLangGet',
|
||||
{'lang': langid, 'format': 'json'},
|
||||
|
|
|
|||
129
dist/ba_data/python/babase/_locale.py
vendored
129
dist/ba_data/python/babase/_locale.py
vendored
|
|
@ -34,13 +34,21 @@ class LocaleSubsystem(AppSubsystem):
|
|||
# the native layer.
|
||||
env = _babase.env()
|
||||
ba_locale = env.get('ba_locale')
|
||||
locale_tag = env.get('locale')
|
||||
if not isinstance(ba_locale, str) or not isinstance(locale_tag, str):
|
||||
raw_locale_tag = env.get('locale')
|
||||
if not isinstance(ba_locale, str) or not isinstance(
|
||||
raw_locale_tag, str
|
||||
):
|
||||
applog.warning(
|
||||
'Seem to be running in a dummy env; using en-US locale-tag.'
|
||||
)
|
||||
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
|
||||
#: 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.)
|
||||
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
|
||||
# English instead.
|
||||
if (
|
||||
self.requires_full_unicode_display(self.default_locale.resolved)
|
||||
and not _babase.supports_unicode_display()
|
||||
):
|
||||
if not self.can_display_locale(self.default_locale):
|
||||
self.default_locale = Locale.ENGLISH
|
||||
|
||||
assert self.can_display_locale(self.default_locale)
|
||||
|
||||
@override
|
||||
def do_apply_app_config(self) -> None:
|
||||
def apply_app_config(self) -> None:
|
||||
""":meta private:"""
|
||||
assert _babase.in_logic_thread()
|
||||
assert isinstance(_babase.app.config, dict)
|
||||
|
|
@ -114,62 +121,68 @@ class LocaleSubsystem(AppSubsystem):
|
|||
|
||||
@staticmethod
|
||||
@cache
|
||||
def requires_full_unicode_display(
|
||||
locale: LocaleResolved,
|
||||
) -> bool:
|
||||
"""Does the locale require full unicode support to display?"""
|
||||
def can_display_locale(locale: Locale) -> bool:
|
||||
"""Are we able to display the passed locale?
|
||||
|
||||
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
|
||||
|
||||
cls = LocaleResolved
|
||||
rlocale = locale.resolved
|
||||
|
||||
# DO need full unicode.
|
||||
if (
|
||||
locale is cls.CHINESE_TRADITIONAL
|
||||
or locale is cls.CHINESE_SIMPLIFIED
|
||||
or locale is cls.ARABIC
|
||||
or locale is cls.HINDI
|
||||
or locale is cls.KOREAN
|
||||
or locale is cls.PERSIAN
|
||||
or locale is cls.TAMIL
|
||||
or locale is cls.THAI
|
||||
or locale is cls.VIETNAMESE
|
||||
rlocale is cls.CHINESE_TRADITIONAL
|
||||
or rlocale is cls.CHINESE_SIMPLIFIED
|
||||
or rlocale is cls.ARABIC
|
||||
or rlocale is cls.HINDI
|
||||
or rlocale is cls.KOREAN
|
||||
or rlocale is cls.PERSIAN
|
||||
or rlocale is cls.TAMIL
|
||||
or rlocale is cls.THAI
|
||||
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
|
||||
|
||||
# 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.
|
||||
assert_never(locale)
|
||||
assert_never(rlocale)
|
||||
|
|
|
|||
49
dist/ba_data/python/babase/_logging.py
vendored
49
dist/ba_data/python/babase/_logging.py
vendored
|
|
@ -6,7 +6,48 @@ from __future__ import annotations
|
|||
|
||||
import logging
|
||||
|
||||
# Our standard set of loggers.
|
||||
balog = logging.getLogger('ba')
|
||||
applog = logging.getLogger('ba.app')
|
||||
lifecyclelog = logging.getLogger('ba.lifecycle')
|
||||
from bacommon.logging import ClientLoggerName
|
||||
|
||||
# Keep a dict of logger descriptions so lookup is speedy, but lazy-init
|
||||
# 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)
|
||||
|
|
|
|||
23
dist/ba_data/python/babase/_login.py
vendored
23
dist/ba_data/python/babase/_login.py
vendored
|
|
@ -12,13 +12,12 @@ from typing import TYPE_CHECKING, final, override
|
|||
|
||||
from bacommon.login import LoginType
|
||||
|
||||
from babase._logging import loginadapterlog
|
||||
import _babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable
|
||||
|
||||
logger = logging.getLogger('ba.loginadapter')
|
||||
|
||||
|
||||
@dataclass
|
||||
class LoginInfo:
|
||||
|
|
@ -97,12 +96,12 @@ class LoginAdapter:
|
|||
return
|
||||
|
||||
if state is None:
|
||||
logger.debug(
|
||||
loginadapterlog.debug(
|
||||
'%s implicit state changed; now signed out.',
|
||||
self.login_type.name,
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
loginadapterlog.debug(
|
||||
'%s implicit state changed; now signed in as %s.',
|
||||
self.login_type.name,
|
||||
state.display_name,
|
||||
|
|
@ -129,7 +128,7 @@ class LoginAdapter:
|
|||
:meta private:
|
||||
"""
|
||||
assert _babase.in_logic_thread()
|
||||
logger.debug(
|
||||
loginadapterlog.debug(
|
||||
'%s adapter got active logins %s.',
|
||||
self.login_type.name,
|
||||
{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_time = now
|
||||
|
||||
logger.debug(
|
||||
loginadapterlog.debug(
|
||||
'%s adapter sign_in() called; fetching sign-in-token...',
|
||||
self.login_type.name,
|
||||
)
|
||||
|
|
@ -207,7 +206,7 @@ class LoginAdapter:
|
|||
|
||||
# Failed to get a sign-in-token.
|
||||
if result is None:
|
||||
logger.debug(
|
||||
loginadapterlog.debug(
|
||||
'%s adapter sign-in-token fetch failed;'
|
||||
' aborting sign-in.',
|
||||
self.login_type.name,
|
||||
|
|
@ -224,7 +223,7 @@ class LoginAdapter:
|
|||
# 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
|
||||
# success.
|
||||
logger.debug(
|
||||
loginadapterlog.debug(
|
||||
'%s adapter sign-in-token fetch succeeded;'
|
||||
' passing to cloud for verification...',
|
||||
self.login_type.name,
|
||||
|
|
@ -235,7 +234,7 @@ class LoginAdapter:
|
|||
) -> None:
|
||||
# This likely means we couldn't communicate with the server.
|
||||
if isinstance(response, Exception):
|
||||
logger.debug(
|
||||
loginadapterlog.debug(
|
||||
'%s adapter got error sign-in response: %s',
|
||||
self.login_type.name,
|
||||
response,
|
||||
|
|
@ -248,7 +247,7 @@ class LoginAdapter:
|
|||
RuntimeError('Sign-in-token was rejected.')
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
loginadapterlog.debug(
|
||||
'%s adapter got successful sign-in response',
|
||||
self.login_type.name,
|
||||
)
|
||||
|
|
@ -298,7 +297,7 @@ class LoginAdapter:
|
|||
# any existing state so it can properly respond to this.
|
||||
if self._implicit_login_state_dirty and self._on_app_loading_called:
|
||||
|
||||
logger.debug(
|
||||
loginadapterlog.debug(
|
||||
'%s adapter sending implicit-state-changed to app.',
|
||||
self.login_type.name,
|
||||
)
|
||||
|
|
@ -322,7 +321,7 @@ class LoginAdapter:
|
|||
self._implicit_login_state.login_id == self._active_login_id
|
||||
)
|
||||
if was_active != is_active:
|
||||
logger.debug(
|
||||
loginadapterlog.debug(
|
||||
'%s adapter back-end-active is now %s.',
|
||||
self.login_type.name,
|
||||
is_active,
|
||||
|
|
|
|||
29
dist/ba_data/python/babase/_meta.py
vendored
29
dist/ba_data/python/babase/_meta.py
vendored
|
|
@ -23,6 +23,8 @@ if TYPE_CHECKING:
|
|||
# This is purely a convenience; it is possible to use full class paths
|
||||
# instead of these or to make the meta system aware of arbitrary classes.
|
||||
EXPORT_CLASS_NAME_SHORTCUTS: dict[str, str] = {
|
||||
# DEPRECATED as of 6/2025. Currently am warning if finding these
|
||||
# but should take this out eventually.
|
||||
'plugin': 'babase.Plugin',
|
||||
# DEPRECATED as of 12/2023. Currently am warning if finding these
|
||||
# but should take this out eventually.
|
||||
|
|
@ -42,12 +44,6 @@ class ScanResults:
|
|||
"""Return exports matching a given 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:
|
||||
"""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:
|
||||
"""Proceed to the extra_scan_dirs portion of the scan.
|
||||
|
|
@ -130,8 +126,7 @@ class MetadataSubsystem:
|
|||
cls,
|
||||
completion_cb,
|
||||
completion_cb_in_bg_thread,
|
||||
),
|
||||
daemon=True,
|
||||
)
|
||||
).start()
|
||||
|
||||
def _load_exported_classes[T](
|
||||
|
|
@ -436,8 +431,20 @@ class DirectoryScan:
|
|||
if export_class_name is not None:
|
||||
classname = modulename + '.' + export_class_name
|
||||
|
||||
# Migrating away from the 'keyboard' name shortcut
|
||||
# since it's specific to bauiv1; warn if we find it.
|
||||
# Migrating away from the 'plugin' name shortcut;
|
||||
# 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':
|
||||
logging.warning(
|
||||
"metascan: %s:%d: '# ba_meta export"
|
||||
|
|
|
|||
1
dist/ba_data/python/babase/_mgen/enums.py
vendored
1
dist/ba_data/python/babase/_mgen/enums.py
vendored
|
|
@ -184,3 +184,4 @@ class SpecialChar(Enum):
|
|||
FLAG_CHILE = 94
|
||||
MIKIROG = 95
|
||||
V2_LOGO = 96
|
||||
CLOSE = 97
|
||||
|
|
|
|||
30
dist/ba_data/python/babase/_net.py
vendored
30
dist/ba_data/python/babase/_net.py
vendored
|
|
@ -3,7 +3,6 @@
|
|||
"""Networking related functionality."""
|
||||
from __future__ import annotations
|
||||
|
||||
import ssl
|
||||
import socket
|
||||
import threading
|
||||
import ipaddress
|
||||
|
|
@ -12,17 +11,24 @@ from typing import TYPE_CHECKING
|
|||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
# Timeout for standard functions talking to the master-server/etc.
|
||||
DEFAULT_REQUEST_TIMEOUT_SECONDS = 60
|
||||
|
||||
|
||||
class NetworkSubsystem:
|
||||
"""Network related app subsystem."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
# Our shared SSL context. Creating these can be expensive so we
|
||||
# create it here once and recycle for our various connections.
|
||||
self.sslcontext = ssl.create_default_context()
|
||||
import babase._env
|
||||
|
||||
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,
|
||||
# as it is updated by a background thread.
|
||||
|
|
@ -34,12 +40,14 @@ class NetworkSubsystem:
|
|||
self.zone_pings: dict[str, float] = {}
|
||||
|
||||
# For debugging/progress.
|
||||
self.v1_test_log: str = ''
|
||||
self.v1_ctest_results: dict[int, str] = {}
|
||||
self.connectivity_state = 'uninited'
|
||||
self.transport_state = 'uninited'
|
||||
self.connectivity_state = ''
|
||||
self.transport_state = ''
|
||||
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:
|
||||
"""Return an address-type given an address.
|
||||
|
|
|
|||
44
dist/ba_data/python/babase/_workspace.py
vendored
44
dist/ba_data/python/babase/_workspace.py
vendored
|
|
@ -54,8 +54,7 @@ class WorkspaceSubsystem:
|
|||
workspaceid=workspaceid,
|
||||
workspacename=workspacename,
|
||||
on_completed=on_completed,
|
||||
),
|
||||
daemon=True,
|
||||
)
|
||||
).start()
|
||||
|
||||
def _errmsg(self, msg: babase.Lstr) -> None:
|
||||
|
|
@ -73,6 +72,7 @@ class WorkspaceSubsystem:
|
|||
workspacename: str,
|
||||
on_completed: Callable[[], None],
|
||||
) -> None:
|
||||
# pylint: disable=too-many-locals
|
||||
from babase._language import Lstr
|
||||
|
||||
class _SkipSyncError(RuntimeError):
|
||||
|
|
@ -83,22 +83,32 @@ class WorkspaceSubsystem:
|
|||
|
||||
set_path = True
|
||||
wspath = Path(
|
||||
_babase.get_volatile_data_directory(), 'workspaces', workspaceid
|
||||
_babase.app.env.cache_directory, 'workspaces', workspaceid
|
||||
)
|
||||
try:
|
||||
# If it seems we're offline, don't even attempt a sync,
|
||||
# but allow using the previous synced state.
|
||||
# (is this a good idea?)
|
||||
# If it seems we're offline, don't even attempt a sync, but
|
||||
# allow using the previous synced state. (is this a good
|
||||
# idea?)
|
||||
if not plus.cloud.is_connected():
|
||||
raise _SkipSyncError()
|
||||
|
||||
manifest = DirectoryManifest.create_from_disk(wspath)
|
||||
|
||||
# FIXME: Should implement a way to pass account credentials in
|
||||
# from the logic thread.
|
||||
# FIXME: Should implement a way to pass account credentials
|
||||
# in from the logic thread.
|
||||
state = bacommon.cloud.WorkspaceFetchState(manifest=manifest)
|
||||
|
||||
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:
|
||||
response = plus.cloud.send_message(
|
||||
bacommon.cloud.WorkspaceFetchMessage(
|
||||
|
|
@ -144,8 +154,8 @@ class WorkspaceSubsystem:
|
|||
)
|
||||
|
||||
except CleanError as exc:
|
||||
# Avoid reusing existing if we fail in the middle; could
|
||||
# be in wonky state.
|
||||
# Avoid reusing existing if we fail in the middle; could be
|
||||
# in wonky state.
|
||||
set_path = False
|
||||
_babase.pushcall(
|
||||
partial(self._errmsg, Lstr(value=str(exc))),
|
||||
|
|
@ -167,8 +177,8 @@ class WorkspaceSubsystem:
|
|||
)
|
||||
|
||||
if set_path and wspath.is_dir():
|
||||
# Add to Python paths and also to list of stuff to be scanned
|
||||
# for meta tags.
|
||||
# Add to Python paths and also to list of stuff to be
|
||||
# scanned for meta tags.
|
||||
sys.path.insert(0, 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."""
|
||||
for fname, fdata in downloads_inline.items():
|
||||
fname = os.path.join(workspace_dir, fname)
|
||||
# If there's a directory where we want our file to go, clear it
|
||||
# out first. File deletes should have run before this so
|
||||
# everything under it should be empty and thus killable via rmdir.
|
||||
# If there's a directory where we want our file to go, clear
|
||||
# it out first. File deletes should have run before this so
|
||||
# everything under it should be empty and thus killable via
|
||||
# rmdir.
|
||||
if os.path.isdir(fname):
|
||||
for basename, dirnames, _fn in os.walk(fname, topdown=False):
|
||||
for dirname in dirnames:
|
||||
|
|
@ -208,7 +219,8 @@ class WorkspaceSubsystem:
|
|||
|
||||
def _handle_dir_prune_empty(self, prunedir: str) -> None:
|
||||
"""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):
|
||||
# It seems that child dirs we kill during the walk are still
|
||||
# listed when the parent dir is visited, so lets make sure
|
||||
|
|
|
|||
13
dist/ba_data/python/baclassic/__init__.py
vendored
13
dist/ba_data/python/baclassic/__init__.py
vendored
|
|
@ -11,14 +11,14 @@ designed in a more modular way.
|
|||
|
||||
# ba_meta require api 9
|
||||
|
||||
# Note: Code relying on classic should import things from here *only*
|
||||
# for type-checking and use the versions in ba*.app.classic at runtime;
|
||||
# that way type-checking will cleanly cover the classic-not-present case
|
||||
# (ba*.app.classic being None).
|
||||
# Note: Stuff in this module mostly exists for type-checking and docs
|
||||
# generation and should generally not be imported or used at runtime.
|
||||
# Generally all interaction with this feature-set should go through
|
||||
# `ba*.app.classic`.
|
||||
|
||||
import logging
|
||||
|
||||
# from efro.util import set_canonical_module_names
|
||||
|
||||
from _baclassic import reload_hooks
|
||||
from baclassic._appmode import ClassicAppMode
|
||||
from baclassic._appsubsystem import ClassicAppSubsystem
|
||||
from baclassic._achievement import Achievement, AchievementSubsystem
|
||||
|
|
@ -40,6 +40,7 @@ __all__ = [
|
|||
'AchievementSubsystem',
|
||||
'show_display_item',
|
||||
'MusicPlayer',
|
||||
'reload_hooks',
|
||||
]
|
||||
|
||||
# We want stuff here to show up as packagename.Foo instead of
|
||||
|
|
|
|||
27
dist/ba_data/python/baclassic/_accountv1.py
vendored
27
dist/ba_data/python/baclassic/_accountv1.py
vendored
|
|
@ -168,9 +168,10 @@ class AccountV1Subsystem:
|
|||
"""(internal)"""
|
||||
# pylint: disable=cyclic-import
|
||||
plus = babase.app.plus
|
||||
if plus is None:
|
||||
classic = babase.app.classic
|
||||
if plus is None or classic is None:
|
||||
return []
|
||||
if plus.get_v1_account_state() != 'signed_in':
|
||||
if plus.accounts.primary is None:
|
||||
return []
|
||||
icons = []
|
||||
store_items: dict[str, Any] = (
|
||||
|
|
@ -179,9 +180,10 @@ class AccountV1Subsystem:
|
|||
else {}
|
||||
)
|
||||
for item_name, item in list(store_items.items()):
|
||||
if item_name.startswith(
|
||||
'icons.'
|
||||
) and plus.get_v1_account_product_purchased(item_name):
|
||||
if (
|
||||
item_name.startswith('icons.')
|
||||
and item_name in classic.purchases
|
||||
):
|
||||
icons.append(item['icon'])
|
||||
return icons
|
||||
|
||||
|
|
@ -228,16 +230,17 @@ class AccountV1Subsystem:
|
|||
def have_pro(self) -> bool:
|
||||
"""Return whether pro is currently unlocked."""
|
||||
|
||||
plus = babase.app.plus
|
||||
if plus is None:
|
||||
classic = babase.app.classic
|
||||
if classic is None:
|
||||
return False
|
||||
purchases = classic.purchases
|
||||
|
||||
# Check various server-side purchases that mean we have pro.
|
||||
return bool(
|
||||
plus.get_v1_account_product_purchased('gold_pass')
|
||||
or plus.get_v1_account_product_purchased('upgrades.pro')
|
||||
or plus.get_v1_account_product_purchased('static.pro')
|
||||
or plus.get_v1_account_product_purchased('static.pro_sale')
|
||||
return (
|
||||
'gold_pass' in purchases
|
||||
or 'upgrades.pro' in purchases
|
||||
or 'static.pro' in purchases
|
||||
or 'static.pro_sale' in purchases
|
||||
)
|
||||
|
||||
def have_pro_options(self) -> bool:
|
||||
|
|
|
|||
263
dist/ba_data/python/baclassic/_appmode.py
vendored
263
dist/ba_data/python/baclassic/_appmode.py
vendored
|
|
@ -1,14 +1,17 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Contains ClassicAppMode."""
|
||||
# pylint: disable=too-many-lines
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import logging
|
||||
import hashlib
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
# from bacommon.app import AppExperience
|
||||
from efro.error import CommunicationError
|
||||
import bacommon.bs
|
||||
import babase
|
||||
import bauiv1
|
||||
|
|
@ -18,7 +21,7 @@ from bauiv1lib.account.signin import show_sign_in_prompt
|
|||
import _baclassic
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Any, Literal
|
||||
from typing import Callable, Any, Literal, Iterable
|
||||
|
||||
from efro.call import CallbackRegistration
|
||||
import bacommon.cloud
|
||||
|
|
@ -29,7 +32,8 @@ if TYPE_CHECKING:
|
|||
class ClassicAppMode(babase.AppMode):
|
||||
"""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:
|
||||
self._on_primary_account_changed_callback: (
|
||||
|
|
@ -44,11 +48,18 @@ class ClassicAppMode(babase.AppMode):
|
|||
self._have_account_values = False
|
||||
self._have_connectivity = False
|
||||
self._current_account_id: str | None = None
|
||||
self._should_restore_account_display_state = False
|
||||
|
||||
self._purchase_ui_pause: bauiv1.RootUIUpdatePause | None = None
|
||||
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
|
||||
@classmethod
|
||||
def can_handle_intent(cls, intent: babase.AppIntent) -> bool:
|
||||
|
|
@ -152,7 +163,7 @@ class ClassicAppMode(babase.AppMode):
|
|||
classic = babase.app.classic
|
||||
|
||||
# Store latest league vis vals for any active account.
|
||||
self._save_account_display_state()
|
||||
self._save_account_state()
|
||||
|
||||
# Stop being informed of account changes.
|
||||
self._on_primary_account_changed_callback = None
|
||||
|
|
@ -173,13 +184,14 @@ class ClassicAppMode(babase.AppMode):
|
|||
@override
|
||||
def on_app_active_changed(self) -> None:
|
||||
if not babase.app.active:
|
||||
# If we've gone inactive, bring up the main menu, which has the
|
||||
# side effect of pausing the action (when possible).
|
||||
babase.invoke_main_menu()
|
||||
# If we're going inactive, ask for the main ui, which should
|
||||
# have the side effect of pausing the action if we're in a
|
||||
# game.
|
||||
babase.request_main_ui()
|
||||
|
||||
# Also store any league vis state for the active account.
|
||||
# this may be our last chance to do this on mobile.
|
||||
self._save_account_display_state()
|
||||
self._save_account_state()
|
||||
|
||||
@override
|
||||
def on_purchase_process_begin(
|
||||
|
|
@ -304,7 +316,7 @@ class ClassicAppMode(babase.AppMode):
|
|||
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:
|
||||
"""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
|
||||
# to see animations for league improvements or other changes
|
||||
# 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(
|
||||
self, account: babase.AccountV2Handle | None
|
||||
|
|
@ -331,17 +415,10 @@ class ClassicAppMode(babase.AppMode):
|
|||
|
||||
if account is not None:
|
||||
self._current_account_id = account.accountid
|
||||
babase.set_ui_account_state(True, account.tag)
|
||||
self._should_restore_account_display_state = True
|
||||
self._restore_account_state()
|
||||
else:
|
||||
# If we had an account, save any existing league vis state
|
||||
# so we'll properly animate to new values the next time we
|
||||
# sign in.
|
||||
self._save_account_display_state()
|
||||
|
||||
self._save_account_state()
|
||||
self._current_account_id = None
|
||||
babase.set_ui_account_state(False)
|
||||
self._should_restore_account_display_state = False
|
||||
|
||||
# For testing subscription functionality.
|
||||
if os.environ.get('BA_SUBSCRIPTION_TEST') == '1':
|
||||
|
|
@ -358,8 +435,11 @@ class ClassicAppMode(babase.AppMode):
|
|||
if account is None:
|
||||
classic.gold_pass = False
|
||||
classic.tokens = 0
|
||||
classic.tickets = 0
|
||||
classic.purchases = frozenset()
|
||||
classic.chest_dock_full = False
|
||||
classic.remove_ads = False
|
||||
self._target_purchases_state = None
|
||||
self._account_data_sub = None
|
||||
_baclassic.set_root_ui_account_values(
|
||||
tickets=-1,
|
||||
|
|
@ -399,6 +479,8 @@ class ClassicAppMode(babase.AppMode):
|
|||
self._update_ui_live_state()
|
||||
|
||||
else:
|
||||
# Establish a subscription to inform us whenever basic stuff
|
||||
# (token count, chests, etc) changes.
|
||||
with account:
|
||||
self._account_data_sub = (
|
||||
plus.cloud.subscribe_classic_account_data(
|
||||
|
|
@ -429,9 +511,8 @@ class ClassicAppMode(babase.AppMode):
|
|||
self, val: bacommon.bs.ClassicAccountLiveData
|
||||
) -> None:
|
||||
achp = round(val.achievements / max(val.achievements_total, 1) * 100.0)
|
||||
# ibc = str(val.inbox_count)
|
||||
# if val.inbox_count_is_max:
|
||||
# ibc += '+'
|
||||
|
||||
babase.accountlog.debug('Got new classic account data.')
|
||||
|
||||
chest0 = val.chests.get('0')
|
||||
chest1 = val.chests.get('1')
|
||||
|
|
@ -445,6 +526,55 @@ class ClassicAppMode(babase.AppMode):
|
|||
classic.remove_ads = val.remove_ads
|
||||
classic.gold_pass = val.gold_pass
|
||||
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 = (
|
||||
chest0 is not None
|
||||
and chest1 is not None
|
||||
|
|
@ -540,21 +670,13 @@ class ClassicAppMode(babase.AppMode):
|
|||
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.
|
||||
self._have_account_values = True
|
||||
self._update_ui_live_state()
|
||||
|
||||
def _root_ui_menu_press(self) -> None:
|
||||
from babase import push_back_press
|
||||
from babase import menu_press
|
||||
|
||||
ui = babase.app.ui_v1
|
||||
|
||||
|
|
@ -562,15 +684,16 @@ class ClassicAppMode(babase.AppMode):
|
|||
old_window = ui.get_main_window()
|
||||
if old_window is not None:
|
||||
|
||||
bauiv1.getsound('swish').play()
|
||||
|
||||
classic = babase.app.classic
|
||||
assert classic is not None
|
||||
classic.resume()
|
||||
|
||||
ui.clear_main_window()
|
||||
return
|
||||
|
||||
# Otherwise
|
||||
push_back_press()
|
||||
else:
|
||||
# Otherwise act like a standard menu button.
|
||||
menu_press()
|
||||
|
||||
def _root_ui_account_press(self) -> None:
|
||||
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
|
||||
# currently displaying for it in the root ui/etc. We'll then
|
||||
# restore that state as a starting point the next time we are
|
||||
# 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.
|
||||
vals = _baclassic.get_account_state()
|
||||
if vals is None:
|
||||
return
|
||||
|
||||
if self._current_account_id is not None:
|
||||
vals = _baclassic.get_account_display_state()
|
||||
if vals is not None:
|
||||
# Stuff our account id in there and save it to our
|
||||
# config.
|
||||
# Stuff some vals of our own in the dict and save to config.
|
||||
assert 'a' not in vals
|
||||
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[self._LEAGUE_VIS_VALS_CONFIG_KEY] = vals
|
||||
cfg[self._ACCOUNT_STATE_CONFIG_KEY] = vals
|
||||
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
|
||||
vals = cfg.get(self._LEAGUE_VIS_VALS_CONFIG_KEY)
|
||||
if isinstance(vals, dict):
|
||||
valsaccount = vals.get('a')
|
||||
vals = cfg.get(self._ACCOUNT_STATE_CONFIG_KEY)
|
||||
|
||||
if not isinstance(vals, dict):
|
||||
return
|
||||
|
||||
# If the state applies to someone else, skip it.
|
||||
accountid = vals.get('a')
|
||||
if (
|
||||
isinstance(valsaccount, str)
|
||||
and valsaccount == self._current_account_id
|
||||
not isinstance(accountid, str)
|
||||
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)
|
||||
|
|
|
|||
88
dist/ba_data/python/baclassic/_appsubsystem.py
vendored
88
dist/ba_data/python/baclassic/_appsubsystem.py
vendored
|
|
@ -1,5 +1,7 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
# pylint: disable=too-many-lines
|
||||
|
||||
"""Provides classic app subsystem."""
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -16,7 +18,6 @@ import bascenev1
|
|||
import _baclassic
|
||||
from baclassic._music import MusicSubsystem
|
||||
from baclassic._accountv1 import AccountV1Subsystem
|
||||
from baclassic._ads import AdsSubsystem
|
||||
from baclassic._net import MasterServerResponseType, MasterServerV1CallThread
|
||||
from baclassic._achievement import AchievementSubsystem
|
||||
from baclassic._tips import get_all_tips
|
||||
|
|
@ -52,7 +53,6 @@ class ClassicAppSubsystem(babase.AppSubsystem):
|
|||
self._env = babase.env()
|
||||
|
||||
self.accounts = AccountV1Subsystem()
|
||||
self.ads = AdsSubsystem()
|
||||
self.ach = AchievementSubsystem()
|
||||
self.store = StoreSubsystem()
|
||||
self.music = MusicSubsystem()
|
||||
|
|
@ -77,11 +77,12 @@ class ClassicAppSubsystem(babase.AppSubsystem):
|
|||
# Classic-specific account state.
|
||||
self.remove_ads = False
|
||||
self.gold_pass = False
|
||||
self.tickets = 0
|
||||
self.tokens = 0
|
||||
self.chest_dock_full = False
|
||||
self.purchases: frozenset[str] = frozenset()
|
||||
|
||||
# Main Menu.
|
||||
self.main_menu_did_initial_transition = False
|
||||
self.main_menu_last_news_fetch_time: float | None = None
|
||||
|
||||
# Spaz.
|
||||
|
|
@ -135,6 +136,31 @@ class ClassicAppSubsystem(babase.AppSubsystem):
|
|||
else:
|
||||
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
|
||||
def platform(self) -> str:
|
||||
"""Name of the current platform.
|
||||
|
|
@ -178,9 +204,12 @@ class ClassicAppSubsystem(babase.AppSubsystem):
|
|||
self.music.on_app_loading()
|
||||
|
||||
# Non-test, non-debug builds should generally be blessed; warn
|
||||
# if not (so I don't accidentally release a build that can't
|
||||
# play tourneys).
|
||||
if not env.debug and not env.test and not plus.is_blessed():
|
||||
# if not (so I don't accidentally release one).
|
||||
if (
|
||||
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))
|
||||
|
||||
stdmaps.register_all_maps()
|
||||
|
|
@ -473,15 +502,13 @@ class ClassicAppSubsystem(babase.AppSubsystem):
|
|||
request: str,
|
||||
data: dict[str, Any],
|
||||
callback: MasterServerCallback | None = None,
|
||||
response_type: MasterServerResponseType = MasterServerResponseType.JSON,
|
||||
) -> None:
|
||||
"""Make a call to the master server via a http GET.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
|
||||
MasterServerV1CallThread(
|
||||
request, 'get', data, callback, response_type
|
||||
request, 'get', data, callback, MasterServerResponseType.JSON
|
||||
).start()
|
||||
|
||||
def master_server_v1_post(
|
||||
|
|
@ -489,14 +516,13 @@ class ClassicAppSubsystem(babase.AppSubsystem):
|
|||
request: str,
|
||||
data: dict[str, Any],
|
||||
callback: MasterServerCallback | None = None,
|
||||
response_type: MasterServerResponseType = MasterServerResponseType.JSON,
|
||||
) -> None:
|
||||
"""Make a call to the master server via a http POST.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
MasterServerV1CallThread(
|
||||
request, 'post', data, callback, response_type
|
||||
request, 'post', data, callback, MasterServerResponseType.JSON
|
||||
).start()
|
||||
|
||||
def set_tournament_prize_image(
|
||||
|
|
@ -658,12 +684,6 @@ class ClassicAppSubsystem(babase.AppSubsystem):
|
|||
|
||||
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:
|
||||
"""(internal)"""
|
||||
from bauiv1lib.serverdialog import (
|
||||
|
|
@ -788,27 +808,29 @@ class ClassicAppSubsystem(babase.AppSubsystem):
|
|||
else:
|
||||
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)"""
|
||||
from bauiv1lib.ingamemenu import InGameMenuWindow
|
||||
from bauiv1 import set_ui_input_device
|
||||
|
||||
assert babase.app is not None
|
||||
in_main_menu = babase.app.ui_v1.has_main_window()
|
||||
if not in_main_menu:
|
||||
set_ui_input_device(device_id)
|
||||
if not babase.app.ui_v1.has_main_window():
|
||||
|
||||
# Hack(ish). We play swish sound here so it happens for
|
||||
# device presses, but this means we need to disable default
|
||||
# swish sounds for any menu buttons or we'll get double.
|
||||
# Note: we play a swish here for when our UI comes in, so we
|
||||
# need to make sure to disable swish sounds for any buttons
|
||||
# that lead us here.
|
||||
if babase.app.env.gui:
|
||||
bauiv1.getsound('swish').play()
|
||||
|
||||
# Pause gameplay.
|
||||
self.pause()
|
||||
|
||||
menu_button = bauiv1.get_special_widget('menu_button')
|
||||
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:
|
||||
|
|
@ -826,7 +848,11 @@ class ClassicAppSubsystem(babase.AppSubsystem):
|
|||
# Bring up the last place we were, or start at the main menu
|
||||
# otherwise.
|
||||
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():
|
||||
|
||||
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
|
||||
# kiosk start screen.
|
||||
if env.demo or env.arcade:
|
||||
if arcade_or_demo:
|
||||
# pylint: disable=cyclic-import
|
||||
from bauiv1lib.kiosk import KioskWindow
|
||||
|
||||
|
|
@ -963,14 +989,14 @@ class ClassicAppSubsystem(babase.AppSubsystem):
|
|||
|
||||
def is_game_unlocked(self, game: str) -> bool:
|
||||
"""Is a particular game unlocked?"""
|
||||
plus = babase.app.plus
|
||||
assert plus is not None
|
||||
classic = babase.app.classic
|
||||
assert classic is not None
|
||||
|
||||
purchases = self.required_purchases_for_game(game)
|
||||
if not purchases:
|
||||
return True
|
||||
|
||||
for purchase in purchases:
|
||||
if not plus.get_v1_account_product_purchased(purchase):
|
||||
if not purchase in classic.purchases:
|
||||
return False
|
||||
return True
|
||||
|
|
|
|||
10
dist/ba_data/python/baclassic/_hooks.py
vendored
10
dist/ba_data/python/baclassic/_hooks.py
vendored
|
|
@ -36,3 +36,13 @@ def on_engine_did_reset() -> None:
|
|||
logging.error(
|
||||
'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()
|
||||
|
|
|
|||
132
dist/ba_data/python/baclassic/_net.py
vendored
132
dist/ba_data/python/baclassic/_net.py
vendored
|
|
@ -3,12 +3,18 @@
|
|||
"""Networking related functionality."""
|
||||
from __future__ import annotations
|
||||
|
||||
import zlib
|
||||
import copy
|
||||
import time
|
||||
import base64
|
||||
import weakref
|
||||
import threading
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
from efro.error import CommunicationError
|
||||
from efro.util import strip_exception_tracebacks
|
||||
import bacommon.bs
|
||||
import babase
|
||||
import bascenev1
|
||||
|
||||
|
|
@ -37,9 +43,7 @@ class MasterServerV1CallThread(threading.Thread):
|
|||
):
|
||||
# pylint: disable=too-many-positional-arguments
|
||||
|
||||
# Set daemon=True so long-running requests don't keep us from
|
||||
# quitting the app.
|
||||
super().__init__(daemon=True)
|
||||
super().__init__()
|
||||
self._request = request
|
||||
self._request_type = request_type
|
||||
if not isinstance(response_type, MasterServerResponseType):
|
||||
|
|
@ -49,9 +53,16 @@ class MasterServerV1CallThread(threading.Thread):
|
|||
self._callback: MasterServerCallback | None = callback
|
||||
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.
|
||||
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:
|
||||
# If we were created in an activity context and that activity
|
||||
|
|
@ -72,23 +83,24 @@ class MasterServerV1CallThread(threading.Thread):
|
|||
self._callback(arg)
|
||||
|
||||
@override
|
||||
def run(self) -> None:
|
||||
# pylint: disable=consider-using-with
|
||||
# pylint: disable=too-many-branches
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import urllib.error
|
||||
import json
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
f'<MasterServerV1CallThread id={id(self)}'
|
||||
f' request={self._request}>'
|
||||
)
|
||||
|
||||
from efro.error import is_urllib_communication_error
|
||||
@override
|
||||
def run(self) -> None:
|
||||
import urllib.parse
|
||||
import json
|
||||
|
||||
plus = babase.app.plus
|
||||
assert plus is not None
|
||||
response_data: Any = None
|
||||
url: str | None = None
|
||||
|
||||
# Tearing the app down while this is running can lead to
|
||||
# rare crashes in LibSSL, so avoid that if at all possible.
|
||||
starttime = time.monotonic()
|
||||
|
||||
# Disallow shutdown while we're working.
|
||||
if not babase.shutdown_suppress_begin():
|
||||
# App is already shutting down, so we're a no-op.
|
||||
return
|
||||
|
|
@ -98,67 +110,62 @@ class MasterServerV1CallThread(threading.Thread):
|
|||
assert classic is not None
|
||||
self._data = _utf8_all(self._data)
|
||||
babase.set_thread_name('BA_ServerCallThread')
|
||||
if self._request_type == 'get':
|
||||
msaddr = plus.get_master_server_address()
|
||||
dataenc = urllib.parse.urlencode(self._data)
|
||||
url = f'{msaddr}/{self._request}?{dataenc}'
|
||||
assert url is not None
|
||||
response = urllib.request.urlopen(
|
||||
urllib.request.Request(
|
||||
url,
|
||||
None,
|
||||
{'User-Agent': classic.legacy_user_agent_string},
|
||||
),
|
||||
context=babase.app.net.sslcontext,
|
||||
timeout=babase.DEFAULT_REQUEST_TIMEOUT_SECONDS,
|
||||
|
||||
mresponse = plus.cloud.send_message(
|
||||
bacommon.bs.LegacyRequest(
|
||||
self._request,
|
||||
self._request_type,
|
||||
classic.legacy_user_agent_string,
|
||||
dataenc,
|
||||
)
|
||||
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:
|
||||
raise TypeError('Invalid request_type: ' + self._request_type)
|
||||
mrdata = mresponse.data
|
||||
|
||||
# If html request failed.
|
||||
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'':
|
||||
if mrdata is None:
|
||||
response_data = None
|
||||
else:
|
||||
response_data = json.loads(raw_data)
|
||||
else:
|
||||
raise TypeError(f'invalid responsetype: {self._response_type}')
|
||||
assert self._response_type == MasterServerResponseType.JSON
|
||||
response_data = json.loads(mrdata)
|
||||
|
||||
except Exception as exc:
|
||||
duration = time.monotonic() - starttime
|
||||
# Ignore common network errors; note unexpected ones.
|
||||
if not is_urllib_communication_error(exc, url=url):
|
||||
print(
|
||||
f'Error in MasterServerCallThread'
|
||||
f' (url={url},'
|
||||
f' response-type={self._response_type},'
|
||||
f' response-data={response_data}):'
|
||||
if isinstance(exc, CommunicationError):
|
||||
babase.netlog.debug(
|
||||
'Legacy %s request failed in %.3fs (communication error).',
|
||||
self._request,
|
||||
duration,
|
||||
)
|
||||
else:
|
||||
babase.netlog.exception(
|
||||
'Legacy %s request failed in %.3fs.',
|
||||
self._request,
|
||||
duration,
|
||||
)
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
response_data = None
|
||||
|
||||
# We're done with the exception, so strip its tracebacks to
|
||||
# avoid reference cycles.
|
||||
strip_exception_tracebacks(exc)
|
||||
|
||||
finally:
|
||||
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:
|
||||
babase.pushcall(
|
||||
babase.Call(self._run_callback, response_data),
|
||||
|
|
@ -167,7 +174,7 @@ class MasterServerV1CallThread(threading.Thread):
|
|||
|
||||
|
||||
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):
|
||||
return dict(
|
||||
(_utf8_all(key), _utf8_all(value))
|
||||
|
|
@ -178,5 +185,6 @@ def _utf8_all(data: Any) -> Any:
|
|||
if isinstance(data, tuple):
|
||||
return tuple(_utf8_all(element) for element in data)
|
||||
if isinstance(data, str):
|
||||
return data.encode('utf-8', errors='ignore')
|
||||
# return data.encode('utf-8', errors='ignore')
|
||||
return data.encode()
|
||||
return data
|
||||
|
|
|
|||
13
dist/ba_data/python/baclassic/_servermode.py
vendored
13
dist/ba_data/python/baclassic/_servermode.py
vendored
|
|
@ -95,6 +95,7 @@ class ServerController:
|
|||
self._ran_access_check = False
|
||||
self._prep_timer: babase.AppTimer | None = None
|
||||
self._next_stuck_login_warn_time = time.time() + 10.0
|
||||
self._next_connectivity_warn_time = time.time() + 5.0
|
||||
self._first_run = True
|
||||
self._shutdown_reason: ShutdownReason | None = None
|
||||
self._executing_shutdown = False
|
||||
|
|
@ -255,6 +256,18 @@ class ServerController:
|
|||
"""Run in a timer to do prep before beginning to serve."""
|
||||
plus = babase.app.plus
|
||||
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'
|
||||
if not signed_in:
|
||||
# Signing in to the local server account should not take long;
|
||||
|
|
|
|||
34
dist/ba_data/python/baclassic/_store.py
vendored
34
dist/ba_data/python/baclassic/_store.py
vendored
|
|
@ -437,13 +437,14 @@ class StoreSubsystem:
|
|||
def get_available_purchase_count(self, tab: str | None = None) -> int:
|
||||
"""(internal)"""
|
||||
plus = babase.app.plus
|
||||
if plus is None:
|
||||
classic = babase.app.classic
|
||||
if plus is None or classic is None:
|
||||
return 0
|
||||
try:
|
||||
if plus.get_v1_account_state() != 'signed_in':
|
||||
if plus.accounts.primary is None:
|
||||
return 0
|
||||
count = 0
|
||||
our_tickets = plus.get_v1_account_ticket_count()
|
||||
our_tickets = classic.tickets
|
||||
store_data = self.get_store_layout()
|
||||
if tab is not None:
|
||||
tabs = [(tab, store_data[tab])]
|
||||
|
|
@ -463,22 +464,22 @@ class StoreSubsystem:
|
|||
) -> int:
|
||||
plus = babase.app.plus
|
||||
assert plus
|
||||
assert babase.app.classic is not None
|
||||
purchases = babase.app.classic.purchases
|
||||
for section in tabval:
|
||||
for item in section['items']:
|
||||
ticket_cost = plus.get_v1_account_misc_read_val(
|
||||
'price.' + item, None
|
||||
)
|
||||
if ticket_cost is not None:
|
||||
if (
|
||||
our_tickets >= ticket_cost
|
||||
and not plus.get_v1_account_product_purchased(item)
|
||||
):
|
||||
if our_tickets >= ticket_cost and item not in purchases:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
def get_available_sale_time(self, tab: str) -> int | None:
|
||||
"""(internal)"""
|
||||
# pylint: disable=too-many-branches
|
||||
# pylint: disable=too-many-locals
|
||||
# pylint: disable=too-many-nested-blocks
|
||||
plus = babase.app.plus
|
||||
assert plus is not None
|
||||
|
|
@ -488,6 +489,7 @@ class StoreSubsystem:
|
|||
|
||||
app = babase.app
|
||||
assert app.classic is not None
|
||||
purchases = app.classic.purchases
|
||||
sale_times: list[int | None] = []
|
||||
|
||||
# Calc time for our pro sale (old special case).
|
||||
|
|
@ -546,7 +548,7 @@ class StoreSubsystem:
|
|||
for section in store_layout[tab]:
|
||||
for item in section['items']:
|
||||
if item in sales_raw:
|
||||
if not plus.get_v1_account_product_purchased(item):
|
||||
if item not in purchases:
|
||||
to_end = (
|
||||
datetime.datetime.fromtimestamp(
|
||||
sales_raw[item]['e'], datetime.UTC
|
||||
|
|
@ -566,15 +568,13 @@ class StoreSubsystem:
|
|||
|
||||
def get_unowned_maps(self) -> list[str]:
|
||||
"""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()
|
||||
if babase.app.env.gui:
|
||||
for map_section in self.get_store_layout()['maps']:
|
||||
for mapitem in map_section['items']:
|
||||
if (
|
||||
plus is None
|
||||
or not plus.get_v1_account_product_purchased(mapitem)
|
||||
):
|
||||
if mapitem not in purchases:
|
||||
m_info = self.get_store_item(mapitem)
|
||||
unowned_maps.add(m_info['map_type'].name)
|
||||
return sorted(unowned_maps)
|
||||
|
|
@ -582,7 +582,8 @@ class StoreSubsystem:
|
|||
def get_unowned_game_types(self) -> set[type[bascenev1.GameActivity]]:
|
||||
"""Return present game types not owned by the current account."""
|
||||
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()
|
||||
if babase.app.env.gui:
|
||||
for section in self.get_store_layout()['minigames']:
|
||||
|
|
@ -591,10 +592,7 @@ class StoreSubsystem:
|
|||
# Ignore things like infinite onslaught which
|
||||
# aren't actually game types.
|
||||
continue
|
||||
if (
|
||||
plus is None
|
||||
or not plus.get_v1_account_product_purchased(mname)
|
||||
):
|
||||
if mname not in purchases:
|
||||
m_info = self.get_store_item(mname)
|
||||
unowned_games.add(m_info['gametype'])
|
||||
return unowned_games
|
||||
|
|
|
|||
1
dist/ba_data/python/baclassic/_tips.py
vendored
1
dist/ba_data/python/baclassic/_tips.py
vendored
|
|
@ -110,6 +110,7 @@ def get_all_tips() -> list[str]:
|
|||
'If your framerate is choppy, try turning down resolution\nor '
|
||||
'visuals in the game\'s graphics settings.'
|
||||
]
|
||||
|
||||
if (
|
||||
app.classic is not None
|
||||
and app.classic.platform in ('android', 'ios')
|
||||
|
|
|
|||
10
dist/ba_data/python/bacommon/app.py
vendored
10
dist/ba_data/python/bacommon/app.py
vendored
|
|
@ -116,12 +116,12 @@ class AppPlatform(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
|
||||
AppPlatform and AppVariant. Generally platform describes a set of
|
||||
hardware, while variant describes a destination or purpose for the
|
||||
build.
|
||||
Each distinct permutation of an app has a unique combination of
|
||||
:class:`AppPlatform` and ``AppVariant``. Generally platform
|
||||
describes a set of hardware, while variant describes a destination
|
||||
or purpose for the build.
|
||||
"""
|
||||
|
||||
#: Default builds.
|
||||
|
|
|
|||
22
dist/ba_data/python/bacommon/bacloud.py
vendored
22
dist/ba_data/python/bacommon/bacloud.py
vendored
|
|
@ -120,8 +120,8 @@ class ResponseData:
|
|||
tuple[list[str], str, dict] | None, IOAttrs('u', store_default=False)
|
||||
] = None
|
||||
|
||||
#: If present, a list of pathnames that should be gzipped
|
||||
#: and uploaded to an 'uploads_inline' bytes dict in end_command args.
|
||||
#: If present, a list of pathnames that should be gzipped and
|
||||
#: uploaded to an 'uploads_inline' bytes dict in end_command args.
|
||||
#: This should be limited to relatively small files.
|
||||
uploads_inline: Annotated[
|
||||
list[str] | None, IOAttrs('uinl', store_default=False)
|
||||
|
|
@ -138,9 +138,9 @@ class ResponseData:
|
|||
Downloads | None, IOAttrs('dl', store_default=False)
|
||||
] = None
|
||||
|
||||
#: If present, pathnames mapped to gzipped data to
|
||||
#: be written to the client. This should only be used for relatively
|
||||
#: small files as they are all included inline as part of the response.
|
||||
#: If present, pathnames mapped to gzipped data to be written to the
|
||||
#: client. This should only be used for relatively small files as
|
||||
#: they are all included inline as part of the response.
|
||||
downloads_inline: Annotated[
|
||||
dict[str, bytes] | None, IOAttrs('dinl', store_default=False)
|
||||
] = None
|
||||
|
|
@ -153,10 +153,10 @@ class ResponseData:
|
|||
#: If present, url to display to the user.
|
||||
open_url: Annotated[str | None, IOAttrs('url', store_default=False)] = None
|
||||
|
||||
#: If present, a line of input is read and placed into
|
||||
#: end_command args as 'input'. The first value is the prompt printed
|
||||
#: before reading and the second is whether it should be read as a
|
||||
#: password (without echoing to the terminal).
|
||||
#: If present, a line of input is read and placed into end_command
|
||||
#: args as 'input'. The first value is the prompt printed before
|
||||
#: reading and the second is whether it should be read as a password
|
||||
#: (without echoing to the terminal).
|
||||
input_prompt: Annotated[
|
||||
tuple[str, bool] | None, IOAttrs('inp', store_default=False)
|
||||
] = None
|
||||
|
|
@ -170,8 +170,8 @@ class ResponseData:
|
|||
#: End arg for end_message print() call.
|
||||
end_message_end: Annotated[str, IOAttrs('eme', store_default=False)] = '\n'
|
||||
|
||||
#: If present, this command is run with these args at the end
|
||||
#: of response processing.
|
||||
#: If present, this command is run with these args at the end of
|
||||
#: response processing.
|
||||
end_command: Annotated[
|
||||
tuple[str, dict] | None, IOAttrs('ec', store_default=False)
|
||||
] = None
|
||||
|
|
|
|||
105
dist/ba_data/python/bacommon/bs.py
vendored
105
dist/ba_data/python/bacommon/bs.py
vendored
|
|
@ -1,5 +1,6 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
# pylint: disable=too-many-lines
|
||||
"""BombSquad specific bits."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -20,6 +21,31 @@ TOKENS3_COUNT = 1200
|
|||
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
|
||||
@dataclass
|
||||
class PrivatePartyMessage(Message):
|
||||
|
|
@ -44,6 +70,25 @@ class PrivatePartyResponse(Response):
|
|||
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):
|
||||
"""Appearances bombsquad classic chests can have."""
|
||||
|
||||
|
|
@ -108,6 +153,11 @@ class ClassicAccountLiveData:
|
|||
GOLD = 'g'
|
||||
DIAMOND = 'd'
|
||||
|
||||
class Flag(Enum):
|
||||
"""Flags set for our account."""
|
||||
|
||||
ASK_FOR_REVIEW = 'r'
|
||||
|
||||
tickets: Annotated[int, IOAttrs('ti')]
|
||||
|
||||
tokens: Annotated[int, IOAttrs('to')]
|
||||
|
|
@ -131,6 +181,11 @@ class ClassicAccountLiveData:
|
|||
|
||||
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):
|
||||
"""Type ID for each of our subclasses."""
|
||||
|
|
@ -738,7 +793,7 @@ class ClientEffectScreenMessage(ClientEffect):
|
|||
"""Display a screen-message."""
|
||||
|
||||
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)
|
||||
|
||||
@override
|
||||
|
|
@ -957,3 +1012,51 @@ class ChestActionResponse(Response):
|
|||
effects: Annotated[
|
||||
list[ClientEffect], IOAttrs('fx', store_default=False)
|
||||
] = 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
|
||||
|
|
|
|||
60
dist/ba_data/python/bacommon/cloud.py
vendored
60
dist/ba_data/python/bacommon/cloud.py
vendored
|
|
@ -25,6 +25,24 @@ class WebLocation(Enum):
|
|||
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
|
||||
@dataclass
|
||||
class LoginProxyRequestMessage(Message):
|
||||
|
|
@ -132,29 +150,6 @@ class TestResponse(Response):
|
|||
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
|
||||
@dataclass
|
||||
class WorkspaceFetchState:
|
||||
|
|
@ -343,3 +338,22 @@ class SecureDataCheckerResponse(Response):
|
|||
"""Here's that checker ya asked for, boss."""
|
||||
|
||||
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')]
|
||||
|
|
|
|||
154
dist/ba_data/python/bacommon/locale.py
vendored
154
dist/ba_data/python/bacommon/locale.py
vendored
|
|
@ -18,9 +18,9 @@ class Locale(Enum):
|
|||
|
||||
This list of locales is considered 'sacred' - we assume any values
|
||||
(and associated long values) added here remain in use out in the
|
||||
wild indefinitely. If a locale value is superseded by a newer or
|
||||
more specific one, the new value should be added and both new and
|
||||
old should map to the same LocaleResolved value.
|
||||
wild indefinitely. If a locale is superseded by a newer or more
|
||||
specific one, the new locale should be added and both new and old
|
||||
should map to the same :class:`LocaleResolved`.
|
||||
"""
|
||||
|
||||
# Locale values are not iso codes or anything specific; just
|
||||
|
|
@ -70,6 +70,7 @@ class Locale(Enum):
|
|||
UKRAINIAN = 'ukrn'
|
||||
VENETIAN = 'venetn'
|
||||
VIETNAMESE = 'viet'
|
||||
KAZAKH = 'kazk'
|
||||
|
||||
# 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
|
||||
|
|
@ -172,6 +173,8 @@ class Locale(Enum):
|
|||
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)
|
||||
|
|
@ -192,6 +195,111 @@ class Locale(Enum):
|
|||
except KeyError as 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
|
||||
def resolved(self) -> LocaleResolved:
|
||||
"""Return the associated resolved locale."""
|
||||
|
|
@ -279,6 +387,8 @@ class Locale(Enum):
|
|||
return R.VENETIAN
|
||||
if self is cls.VIETNAMESE:
|
||||
return R.VIETNAMESE
|
||||
if self is cls.KAZAKH:
|
||||
return R.KAZAKH
|
||||
|
||||
# Make sure we're covering all cases.
|
||||
assert_never(self)
|
||||
|
|
@ -333,6 +443,7 @@ class LocaleResolved(Enum):
|
|||
UKRAINIAN = 'ukrn'
|
||||
VENETIAN = 'venetn'
|
||||
VIETNAMESE = 'viet'
|
||||
KAZAKH = 'kazk'
|
||||
|
||||
# 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
|
||||
|
|
@ -433,6 +544,8 @@ class LocaleResolved(Enum):
|
|||
return Locale.VENETIAN
|
||||
if self is cls.VIETNAMESE:
|
||||
return Locale.VIETNAMESE
|
||||
if self is cls.KAZAKH:
|
||||
return Locale.KAZAKH
|
||||
|
||||
# Make sure we're covering all cases.
|
||||
assert_never(self)
|
||||
|
|
@ -532,6 +645,8 @@ class LocaleResolved(Enum):
|
|||
val = 'vec'
|
||||
elif self is cls.VIETNAMESE:
|
||||
val = 'vi'
|
||||
elif self is cls.KAZAKH:
|
||||
val = 'kk'
|
||||
else:
|
||||
# Make sure we cover all cases.
|
||||
assert_never(self)
|
||||
|
|
@ -627,19 +742,20 @@ class LocaleResolved(Enum):
|
|||
if not extras or any(
|
||||
val in extras
|
||||
for val in [
|
||||
'419',
|
||||
'mx',
|
||||
'ar',
|
||||
'co',
|
||||
'cl',
|
||||
'pe',
|
||||
've',
|
||||
'cr',
|
||||
'pr',
|
||||
'do',
|
||||
'uy',
|
||||
'ec',
|
||||
'pa',
|
||||
'419', # Latin America / Carribean region
|
||||
'mx', # Mexico
|
||||
'ar', # Argentina
|
||||
'co', # Colombia
|
||||
'cl', # Chile
|
||||
'pe', # Peru
|
||||
've', # Venezuela
|
||||
'cr', # Costa Rica
|
||||
'pr', # Puerto Rico
|
||||
'do', # Dominican Republic
|
||||
'uy', # Uruguay
|
||||
'ec', # Ecuador
|
||||
'pa', # Panama
|
||||
'bo', # Bolivia
|
||||
]
|
||||
):
|
||||
return cls.SPANISH_LATIN_AMERICA
|
||||
|
|
@ -656,6 +772,10 @@ class LocaleResolved(Enum):
|
|||
fallback.name,
|
||||
)
|
||||
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':
|
||||
return cls.ARABIC
|
||||
if lang == 'be':
|
||||
|
|
@ -716,6 +836,8 @@ class LocaleResolved(Enum):
|
|||
return cls.VENETIAN
|
||||
if lang == 'vi':
|
||||
return cls.VIETNAMESE
|
||||
if lang == 'kk':
|
||||
return cls.KAZAKH
|
||||
|
||||
# Make noise if we come across something unexpected so we can
|
||||
# add it.
|
||||
|
|
|
|||
86
dist/ba_data/python/bacommon/logging.py
vendored
86
dist/ba_data/python/bacommon/logging.py
vendored
|
|
@ -5,15 +5,92 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, assert_never
|
||||
from bacommon.loggercontrol import LoggerControlConfig
|
||||
|
||||
if TYPE_CHECKING:
|
||||
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:
|
||||
"""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
|
||||
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
|
||||
# and whatnot.
|
||||
return LoggerControlConfig(
|
||||
levels={'root': logging.WARNING, 'ba.app': logging.INFO}
|
||||
levels={
|
||||
'root': logging.WARNING,
|
||||
ClientLoggerName.APP.value: logging.INFO,
|
||||
}
|
||||
)
|
||||
|
|
|
|||
9
dist/ba_data/python/bacommon/net.py
vendored
9
dist/ba_data/python/bacommon/net.py
vendored
|
|
@ -20,11 +20,7 @@ class ServerNodeEntry:
|
|||
"""Information about a specific server."""
|
||||
|
||||
zone: Annotated[str, IOAttrs('r')]
|
||||
|
||||
# TODO: Remove soft_default after all master-servers upgraded.
|
||||
latlong: Annotated[
|
||||
tuple[float, float] | None, IOAttrs('ll', soft_default=None)
|
||||
]
|
||||
latlong: Annotated[tuple[float, float] | None, IOAttrs('ll')]
|
||||
address: Annotated[str, IOAttrs('a')]
|
||||
port: Annotated[int, IOAttrs('p')]
|
||||
|
||||
|
|
@ -43,6 +39,9 @@ class ServerNodeQueryResponse:
|
|||
ping_per_dist: Annotated[float, IOAttrs('ppd')]
|
||||
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[
|
||||
float | None, IOAttrs('d', store_default=False)
|
||||
] = None
|
||||
|
|
|
|||
|
|
@ -191,6 +191,15 @@ class ServerConfig:
|
|||
# CRITICAL.
|
||||
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
|
||||
# the child-process should go through these and not ad-hoc Python string
|
||||
|
|
|
|||
|
|
@ -37,18 +37,23 @@ class AssetsV1GlobalVals:
|
|||
] = ''
|
||||
|
||||
|
||||
class AssetsV1StrInputTypeID(Enum):
|
||||
class AssetsV1StringFileTypeID(Enum):
|
||||
"""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."""
|
||||
|
||||
@override
|
||||
@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
|
||||
# full type registry/lookup here it would require us to import
|
||||
# everything and would prevent lazy loading.
|
||||
|
|
@ -57,14 +62,14 @@ class AssetsV1StrInput(IOMultiType[AssetsV1StrInputTypeID]):
|
|||
@override
|
||||
@classmethod
|
||||
def get_type(
|
||||
cls, type_id: AssetsV1StrInputTypeID
|
||||
) -> type[AssetsV1StrInput]:
|
||||
cls, type_id: AssetsV1StringFileTypeID
|
||||
) -> type[AssetsV1StringFile]:
|
||||
"""Return the subclass for each of our type-ids."""
|
||||
# pylint: disable=cyclic-import
|
||||
|
||||
t = AssetsV1StrInputTypeID
|
||||
if type_id is t.BASIC:
|
||||
return BasicV1StrInput
|
||||
t = AssetsV1StringFileTypeID
|
||||
if type_id is t.V1:
|
||||
return AssetsV1StringFileV1
|
||||
|
||||
# Important to make sure we provide all types.
|
||||
assert_never(type_id)
|
||||
|
|
@ -72,42 +77,44 @@ class AssetsV1StrInput(IOMultiType[AssetsV1StrInputTypeID]):
|
|||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class BasicV1StrInput(AssetsV1StrInput):
|
||||
"""Just a test."""
|
||||
class AssetsV1StringFileV1(AssetsV1StringFile):
|
||||
"""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
|
||||
@classmethod
|
||||
def get_type_id(cls) -> AssetsV1StrInputTypeID:
|
||||
return AssetsV1StrInputTypeID.BASIC
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class AssetsV1StrData:
|
||||
"""Data and output for a string asset."""
|
||||
def get_type_id(cls) -> AssetsV1StringFileTypeID:
|
||||
return AssetsV1StringFileTypeID.V1
|
||||
|
||||
@dataclass
|
||||
class Output:
|
||||
"""Represents a single instance of localized output."""
|
||||
"""Represents a single localized output."""
|
||||
|
||||
class Source(Enum):
|
||||
"""Where localized output can come from."""
|
||||
#: When this output was last changed.
|
||||
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')]
|
||||
created: Annotated[datetime.datetime, IOAttrs('c')]
|
||||
source: Annotated[Source, IOAttrs('s')]
|
||||
locale: Annotated[Locale, IOAttrs('l')]
|
||||
value_default: Annotated[
|
||||
str | None, IOAttrs('d', store_default=False)
|
||||
] = None
|
||||
|
||||
inputs: Annotated[list[AssetsV1StrInput], IOAttrs('i')] = field(
|
||||
default_factory=list
|
||||
input: Annotated[str, IOAttrs('input')]
|
||||
input_modtime: Annotated[
|
||||
datetime.datetime, IOAttrs('input_modtime', float_times=True)
|
||||
]
|
||||
style_preset: Annotated[
|
||||
StylePreset, IOAttrs('style_preset', store_default=False)
|
||||
] = StylePreset.NONE
|
||||
outputs: Annotated[dict[Locale, Output], IOAttrs('outputs')] = field(
|
||||
default_factory=dict
|
||||
)
|
||||
outputs: Annotated[list[Output], IOAttrs('o')] = field(default_factory=list)
|
||||
next_output_id: Annotated[int, IOAttrs('n')] = 0
|
||||
|
||||
|
||||
class AssetsV1PathValsTypeID(Enum):
|
||||
|
|
|
|||
171
dist/ba_data/python/baenv.py
vendored
171
dist/ba_data/python/baenv.py
vendored
|
|
@ -19,6 +19,7 @@ from __future__ import annotations
|
|||
import os
|
||||
import sys
|
||||
import time
|
||||
import random
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
|
|
@ -26,10 +27,12 @@ from typing import TYPE_CHECKING
|
|||
import __main__
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
from typing import Any, Callable
|
||||
|
||||
from efro.logging import LogHandler
|
||||
|
||||
logger = logging.getLogger('ba.env')
|
||||
|
||||
# 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
|
||||
# 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).
|
||||
# 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
|
||||
# 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.
|
||||
#
|
||||
# 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
|
||||
# using.
|
||||
TARGET_BALLISTICA_BUILD = 22381
|
||||
TARGET_BALLISTICA_VERSION = '1.7.41'
|
||||
TARGET_BALLISTICA_BUILD = 22535
|
||||
TARGET_BALLISTICA_VERSION = '1.7.51'
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -67,6 +70,10 @@ class EnvConfig:
|
|||
#: Directory containing ba_data and any other platform-specific data.
|
||||
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.
|
||||
app_python_dir: str | None
|
||||
|
||||
|
|
@ -76,20 +83,19 @@ class EnvConfig:
|
|||
#: Where the app's bundled third party Python stuff lives.
|
||||
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
|
||||
|
||||
#: We have a mechanism allowing app scripts to be overridden by
|
||||
#: placing a specially named directory in a user-scripts dir. This is
|
||||
#: true if that is enabled.
|
||||
#: We have a mechanism allowing :attr:`app_python_dir` to be
|
||||
#: overridden by placing a specially named directory in
|
||||
#: :attr:`user_python_dir`. This is true if that is enabled.
|
||||
is_user_app_python_dir: bool
|
||||
|
||||
#: Our fancy app log handler. This handles feeding logs, stdout, and
|
||||
#: stderr into the engine so they show up on in-app consoles, etc.
|
||||
log_handler: LogHandler | None
|
||||
|
||||
#: Initial data from the config.json file in the config dir. The
|
||||
#: config file is parsed by
|
||||
# Initial data from the ``config.json`` file in the config dir.
|
||||
initial_app_config: Any
|
||||
|
||||
#: Timestamp when we first started doing stuff.
|
||||
|
|
@ -122,17 +128,20 @@ class _EnvGlobals:
|
|||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def config_exists() -> bool:
|
||||
def env_config_exists() -> bool:
|
||||
"""Has a config been created?"""
|
||||
|
||||
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."""
|
||||
envglobals = _EnvGlobals.get()
|
||||
|
||||
|
|
@ -143,7 +152,7 @@ def get_config() -> EnvConfig:
|
|||
# paths to run Ballistica apps should be explicitly calling
|
||||
# configure() first to get a full featured setup.
|
||||
if not envglobals.called_configure:
|
||||
configure(setup_logging=False)
|
||||
configure(setup_logging=False, setup_pycache_prefix=False)
|
||||
|
||||
config = envglobals.config
|
||||
if config is None:
|
||||
|
|
@ -161,8 +170,11 @@ def configure(
|
|||
user_python_dir: str | None = None,
|
||||
app_python_dir: str | None = None,
|
||||
site_python_dir: str | None = None,
|
||||
cache_dir: str | None = None,
|
||||
contains_python_dist: bool = False,
|
||||
setup_logging: bool = True,
|
||||
setup_pycache_prefix: bool = False,
|
||||
strict_threads_atexit: Callable[[Callable[[], None]], None] | None = None,
|
||||
) -> None:
|
||||
"""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
|
||||
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
|
||||
# relative times in our log timestamp displays and also pass this to
|
||||
|
|
@ -200,6 +213,7 @@ def configure(
|
|||
site_python_dir,
|
||||
data_dir,
|
||||
config_dir,
|
||||
cache_dir,
|
||||
standard_app_python_dir,
|
||||
is_user_app_python_dir,
|
||||
) = _setup_paths(
|
||||
|
|
@ -208,14 +222,27 @@ def configure(
|
|||
site_python_dir,
|
||||
data_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.
|
||||
# Later, once the engine comes up, the handler will feed its logs
|
||||
# (including cached history) to the os-specific output location.
|
||||
# This means anything printed or logged at this point forward should
|
||||
# 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.
|
||||
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.
|
||||
if sys.flags.utf8_mode != 1:
|
||||
logging.warning(
|
||||
logger.warning(
|
||||
"Python's UTF-8 mode is not set. Running Ballistica without"
|
||||
' 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.
|
||||
_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.
|
||||
_setup_certs(contains_python_dist)
|
||||
|
|
@ -241,6 +299,7 @@ def configure(
|
|||
envglobals.config = EnvConfig(
|
||||
config_dir=config_dir,
|
||||
data_dir=data_dir,
|
||||
cache_dir=cache_dir,
|
||||
user_python_dir=user_python_dir,
|
||||
app_python_dir=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:
|
||||
"""Read the app config."""
|
||||
import json
|
||||
|
|
@ -269,7 +343,7 @@ def _read_app_config(config_file_path: str) -> dict:
|
|||
config = {}
|
||||
|
||||
except Exception:
|
||||
logging.exception(
|
||||
logger.exception(
|
||||
"Error reading config file '%s'.\n"
|
||||
"Backing up broken config to'%s.broken'.",
|
||||
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')
|
||||
except Exception:
|
||||
logging.exception('Error copying broken config.')
|
||||
logger.exception('Error copying broken config.')
|
||||
config = {}
|
||||
|
||||
return config
|
||||
|
|
@ -310,7 +384,10 @@ def _calc_data_dir(data_dir: str | None) -> str:
|
|||
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
|
||||
|
||||
log_handler = setup_logging(
|
||||
|
|
@ -319,7 +396,18 @@ def _create_log_handler(launch_time: float) -> LogHandler:
|
|||
log_stdout_stderr=True,
|
||||
cache_size_limit=1024 * 1024,
|
||||
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
|
||||
|
||||
|
||||
|
|
@ -359,7 +447,7 @@ def _set_log_levels(app_config: dict) -> None:
|
|||
).apply()
|
||||
|
||||
except Exception:
|
||||
logging.exception('Error setting log levels.')
|
||||
logger.exception('Error setting log levels.')
|
||||
|
||||
|
||||
def _setup_certs(contains_python_dist: bool) -> None:
|
||||
|
|
@ -386,7 +474,10 @@ def _setup_paths(
|
|||
site_python_dir: str | None,
|
||||
data_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
|
||||
# Python imports:
|
||||
|
||||
|
|
@ -398,6 +489,10 @@ def _setup_paths(
|
|||
if config_dir is None:
|
||||
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 = str(Path(data_dir, 'ba_data', 'python'))
|
||||
|
||||
|
|
@ -422,7 +517,8 @@ def _setup_paths(
|
|||
if app_python_dir is None:
|
||||
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:
|
||||
site_python_dir = str(
|
||||
Path(data_dir, 'ba_data', 'python-site-packages')
|
||||
|
|
@ -447,7 +543,7 @@ def _setup_paths(
|
|||
app_python_dir = str(check_dir)
|
||||
is_user_app_python_dir = True
|
||||
except PermissionError:
|
||||
logging.warning(
|
||||
logger.warning(
|
||||
"PermissionError checking user-app-python-dir path '%s'.",
|
||||
check_dir,
|
||||
)
|
||||
|
|
@ -492,14 +588,18 @@ def _setup_paths(
|
|||
site_python_dir,
|
||||
data_dir,
|
||||
config_dir,
|
||||
cache_dir,
|
||||
standard_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]] = [
|
||||
('config', config_dir),
|
||||
('cache', cache_dir),
|
||||
('user_python', user_python_dir),
|
||||
]
|
||||
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)
|
||||
except Exception:
|
||||
# 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
|
||||
)
|
||||
|
||||
|
||||
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.
|
||||
|
||||
The arg flag and value are removed from the arg list. We also check
|
||||
|
|
@ -567,6 +667,7 @@ def _modular_main() -> None:
|
|||
# command line.
|
||||
|
||||
try:
|
||||
|
||||
# Take note that we're running via modular-main. The native
|
||||
# layer can key off this to know whether it should apply
|
||||
# sys.argv or not.
|
||||
|
|
@ -580,20 +681,21 @@ def _modular_main() -> None:
|
|||
args = sys.argv.copy()
|
||||
|
||||
# NOTE: We need to keep these arg long/short arg versions synced
|
||||
# to those in core_config.cc. That code parses these same args
|
||||
# (even if it doesn't handle them in our case) and will complain
|
||||
# if unrecognized args come through.
|
||||
# to those in core_config.cc. That code will parse these same
|
||||
# args (even if it doesn't do anything with them in this modular
|
||||
# path) and will complain if unrecognized args come through.
|
||||
|
||||
# Our -c arg basically mirrors Python's -c arg. If we get that,
|
||||
# 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:
|
||||
exec(command) # pylint: disable=exec-used
|
||||
return
|
||||
|
||||
config_dir = extract_arg(args, ['--config-dir', '-C'], 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)
|
||||
config_dir = _extract_arg(args, ['--config-dir', '-C'], 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)
|
||||
cache_dir = _extract_arg(args, ['--cache-dir', '-a'], is_dir=True)
|
||||
|
||||
# We run configure() BEFORE importing babase. (part of its job
|
||||
# is to wrangle paths which can affect where babase and
|
||||
|
|
@ -602,6 +704,7 @@ def _modular_main() -> None:
|
|||
config_dir=config_dir,
|
||||
data_dir=data_dir,
|
||||
user_python_dir=mods_dir,
|
||||
cache_dir=cache_dir,
|
||||
)
|
||||
|
||||
import babase
|
||||
|
|
|
|||
10
dist/ba_data/python/baplus/__init__.py
vendored
10
dist/ba_data/python/baplus/__init__.py
vendored
|
|
@ -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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
# ba_meta require api 9
|
||||
|
||||
# Note: there's not much here. Most interaction with this feature-set
|
||||
# should go through ba*.app.plus.
|
||||
# Note: Stuff in this module mostly exists for type-checking and docs
|
||||
# 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
|
||||
|
||||
from baplus._cloud import CloudSubsystem
|
||||
from baplus._appsubsystem import PlusAppSubsystem
|
||||
from baplus._ads import AdsSubsystem
|
||||
|
||||
__all__ = [
|
||||
'AdsSubsystem',
|
||||
'CloudSubsystem',
|
||||
'PlusAppSubsystem',
|
||||
]
|
||||
|
|
|
|||
238
dist/ba_data/python/baplus/_ads.py
vendored
Normal file
238
dist/ba_data/python/baplus/_ads.py
vendored
Normal 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
|
||||
104
dist/ba_data/python/baplus/_appsubsystem.py
vendored
104
dist/ba_data/python/baplus/_appsubsystem.py
vendored
|
|
@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, override
|
|||
from babase import AppSubsystem
|
||||
|
||||
import _baplus
|
||||
from baplus._ads import AdsSubsystem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Any
|
||||
|
|
@ -29,13 +30,14 @@ class PlusAppSubsystem(AppSubsystem):
|
|||
|
||||
# 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
|
||||
cloud: CloudSubsystem
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
||||
#: Ad wrangling functionality.
|
||||
self.ads: AdsSubsystem = AdsSubsystem()
|
||||
|
||||
@override
|
||||
def on_app_loading(self) -> None:
|
||||
""":meta private:"""
|
||||
|
|
@ -59,12 +61,36 @@ class PlusAppSubsystem(AppSubsystem):
|
|||
return _baplus.game_service_has_leaderboard(game, config)
|
||||
|
||||
@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.
|
||||
|
||||
: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
|
||||
def get_classic_news_show() -> str:
|
||||
|
|
@ -76,16 +102,6 @@ class PlusAppSubsystem(AppSubsystem):
|
|||
""":meta private:"""
|
||||
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
|
||||
def get_v1_account_display_string(full: bool = True) -> str:
|
||||
""":meta private:"""
|
||||
|
|
@ -126,13 +142,13 @@ class PlusAppSubsystem(AppSubsystem):
|
|||
""":meta private:"""
|
||||
return _baplus.get_v1_account_state_num()
|
||||
|
||||
@staticmethod
|
||||
def get_v1_account_ticket_count() -> int:
|
||||
"""Return the number of tickets for the current account.
|
||||
# @staticmethod
|
||||
# def get_v1_account_ticket_count() -> int:
|
||||
# """Return the number of tickets for the current account.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
return _baplus.get_v1_account_ticket_count()
|
||||
# :meta private:
|
||||
# """
|
||||
# return _baplus.get_v1_account_ticket_count()
|
||||
|
||||
@staticmethod
|
||||
def get_v1_account_type() -> str:
|
||||
|
|
@ -257,50 +273,6 @@ class PlusAppSubsystem(AppSubsystem):
|
|||
"""
|
||||
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
|
||||
def show_game_service_ui(
|
||||
show: str = 'general',
|
||||
|
|
|
|||
121
dist/ba_data/python/baplus/_cloud.py
vendored
121
dist/ba_data/python/baplus/_cloud.py
vendored
|
|
@ -4,17 +4,21 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, overload
|
||||
|
||||
from efro.error import CommunicationError
|
||||
from efro.call import CallbackSet
|
||||
from efro.dataclassio import dataclass_from_dict, dataclass_to_dict
|
||||
import bacommon.bs
|
||||
import bacommon.cloud
|
||||
import babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Any
|
||||
|
||||
from efro.message import Message, Response
|
||||
import bacommon.cloud
|
||||
from efro.message import Message, Response, BoolResponse
|
||||
import bacommon.bs
|
||||
|
||||
|
||||
|
|
@ -31,12 +35,41 @@ class CloudSubsystem(babase.AppSubsystem):
|
|||
:class:`~baplus.PlusAppSubsystem` class.
|
||||
"""
|
||||
|
||||
#: General engine config values provided by the cloud.
|
||||
vals: bacommon.cloud.CloudVals
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.on_connectivity_changed_callbacks: CallbackSet[
|
||||
Callable[[bool], None]
|
||||
] = 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
|
||||
def connected(self) -> bool:
|
||||
"""Whether a connection to the cloud is present.
|
||||
|
|
@ -70,6 +103,55 @@ class CloudSubsystem(babase.AppSubsystem):
|
|||
except Exception:
|
||||
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
|
||||
def send_message_cb(
|
||||
self,
|
||||
|
|
@ -79,6 +161,15 @@ class CloudSubsystem(babase.AppSubsystem):
|
|||
],
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def send_message_cb(
|
||||
self,
|
||||
msg: bacommon.cloud.CloudValsRequest,
|
||||
on_response: Callable[
|
||||
[bacommon.cloud.CloudValsResponse | Exception], None
|
||||
],
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def send_message_cb(
|
||||
self,
|
||||
|
|
@ -120,6 +211,15 @@ class CloudSubsystem(babase.AppSubsystem):
|
|||
],
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def send_message_cb(
|
||||
self,
|
||||
msg: bacommon.bs.GetClassicPurchasesMessage,
|
||||
on_response: Callable[
|
||||
[bacommon.bs.GetClassicPurchasesResponse | Exception], None
|
||||
],
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def send_message_cb(
|
||||
self,
|
||||
|
|
@ -174,6 +274,13 @@ class CloudSubsystem(babase.AppSubsystem):
|
|||
],
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def send_message_cb(
|
||||
self,
|
||||
msg: bacommon.bs.GlobalProfileCheckMessage,
|
||||
on_response: Callable[[BoolResponse | Exception], None],
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def send_message_cb(
|
||||
self,
|
||||
|
|
@ -230,6 +337,11 @@ class CloudSubsystem(babase.AppSubsystem):
|
|||
self, msg: bacommon.cloud.TestMessage
|
||||
) -> bacommon.cloud.TestResponse: ...
|
||||
|
||||
@overload
|
||||
def send_message(
|
||||
self, msg: bacommon.bs.LegacyRequest
|
||||
) -> bacommon.bs.LegacyResponse: ...
|
||||
|
||||
def send_message(self, msg: Message) -> Response | None:
|
||||
"""Synchronously send a message to the cloud.
|
||||
|
||||
|
|
@ -241,8 +353,8 @@ class CloudSubsystem(babase.AppSubsystem):
|
|||
|
||||
@overload
|
||||
async def send_message_async(
|
||||
self, msg: bacommon.cloud.SendInfoMessage
|
||||
) -> bacommon.cloud.SendInfoResponse: ...
|
||||
self, msg: bacommon.bs.SendInfoMessage
|
||||
) -> bacommon.bs.SendInfoResponse: ...
|
||||
|
||||
@overload
|
||||
async def send_message_async(
|
||||
|
|
@ -321,6 +433,7 @@ def cloud_console_exec(code: str) -> None:
|
|||
execcode = compile(code, '<console>', 'exec')
|
||||
# pylint: disable=exec-used
|
||||
exec(execcode, vars(__main__), vars(__main__))
|
||||
|
||||
except Exception:
|
||||
import traceback
|
||||
|
||||
|
|
|
|||
14
dist/ba_data/python/bascenev1/__init__.py
vendored
14
dist/ba_data/python/bascenev1/__init__.py
vendored
|
|
@ -71,7 +71,7 @@ from _bascenev1 import (
|
|||
basetimer,
|
||||
BaseTimer,
|
||||
camerashake,
|
||||
capture_gamepad_input,
|
||||
capture_game_controller_input,
|
||||
capture_keyboard_input,
|
||||
chatmessage,
|
||||
client_info_query_response,
|
||||
|
|
@ -94,7 +94,7 @@ from _bascenev1 import (
|
|||
get_public_party_max_size,
|
||||
get_random_names,
|
||||
get_replay_speed_exponent,
|
||||
get_ui_input_device,
|
||||
get_main_ui_input_device,
|
||||
getactivity,
|
||||
getcollisionmesh,
|
||||
getdata,
|
||||
|
|
@ -122,8 +122,9 @@ from _bascenev1 import (
|
|||
pause_replay,
|
||||
printnodes,
|
||||
protocol_version,
|
||||
release_gamepad_input,
|
||||
release_game_controller_input,
|
||||
release_keyboard_input,
|
||||
reload_hooks,
|
||||
reset_random_player_names,
|
||||
resume_replay,
|
||||
seek_replay,
|
||||
|
|
@ -277,7 +278,7 @@ __all__ = [
|
|||
'cameraflash',
|
||||
'camerashake',
|
||||
'Campaign',
|
||||
'capture_gamepad_input',
|
||||
'capture_game_controller_input',
|
||||
'capture_keyboard_input',
|
||||
'CelebrateMessage',
|
||||
'chatmessage',
|
||||
|
|
@ -346,7 +347,7 @@ __all__ = [
|
|||
'get_remote_app_name',
|
||||
'get_replay_speed_exponent',
|
||||
'get_trophy_string',
|
||||
'get_ui_input_device',
|
||||
'get_main_ui_input_device',
|
||||
'getactivity',
|
||||
'getcollision',
|
||||
'getcollisionmesh',
|
||||
|
|
@ -413,8 +414,9 @@ __all__ = [
|
|||
'protocol_version',
|
||||
'pushcall',
|
||||
'register_map',
|
||||
'release_gamepad_input',
|
||||
'release_game_controller_input',
|
||||
'release_keyboard_input',
|
||||
'reload_hooks',
|
||||
'reset_random_player_names',
|
||||
'resume_replay',
|
||||
'seek_replay',
|
||||
|
|
|
|||
18
dist/ba_data/python/bascenev1/_activitytypes.py
vendored
18
dist/ba_data/python/bascenev1/_activitytypes.py
vendored
|
|
@ -43,16 +43,22 @@ class EndSessionActivity(Activity[EmptyPlayer, EmptyTeam]):
|
|||
def on_begin(self) -> None:
|
||||
# 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()
|
||||
babase.unlock_all_input()
|
||||
assert babase.app.classic is not None
|
||||
babase.app.classic.ads.call_after_ad(
|
||||
babase.Call(_bascenev1.new_host_session, main_menu_session)
|
||||
)
|
||||
assert babase.app.plus is not None
|
||||
|
||||
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]):
|
||||
|
|
|
|||
8
dist/ba_data/python/bascenev1/_coopgame.py
vendored
8
dist/ba_data/python/bascenev1/_coopgame.py
vendored
|
|
@ -57,8 +57,12 @@ class CoopGameActivity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team](
|
|||
super().on_begin()
|
||||
|
||||
# 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(
|
||||
3.8, babase.WeakCall(self._show_remaining_achievements)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -318,13 +318,17 @@ class CoopSession(Session):
|
|||
else:
|
||||
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
|
||||
# and will be going into onslaught-training, show the
|
||||
# tutorial first.
|
||||
if (
|
||||
isinstance(activity, JoinActivity)
|
||||
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:
|
||||
raise RuntimeError('Tutorial not preloaded properly.')
|
||||
|
|
@ -346,7 +350,7 @@ class CoopSession(Session):
|
|||
# Now flip the current activity..
|
||||
self.setactivity(next_game)
|
||||
|
||||
if not (env.demo or env.arcade):
|
||||
if not arcade_or_demo:
|
||||
if (
|
||||
self.tournament_id is not None
|
||||
and classic.coop_session_args['submit_score']
|
||||
|
|
|
|||
47
dist/ba_data/python/bascenev1/_gameactivity.py
vendored
47
dist/ba_data/python/bascenev1/_gameactivity.py
vendored
|
|
@ -7,6 +7,8 @@ from __future__ import annotations
|
|||
|
||||
import random
|
||||
import logging
|
||||
import time
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
import babase
|
||||
|
|
@ -338,8 +340,27 @@ class GameActivity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team](
|
|||
# Make our map.
|
||||
self._map = self._map_type()
|
||||
|
||||
# Give our map a chance to override the music.
|
||||
# (for happy-thoughts and other such themed maps)
|
||||
# Add default activities for our map.
|
||||
mapname = getattr(self._map_type, 'name', None)
|
||||
map_preview = getattr(self._map_type, 'get_preview_texture_name', None)
|
||||
|
||||
if babase.app.discord.is_ready and mapname and map_preview:
|
||||
preview = map_preview().lower().removesuffix('preview')
|
||||
babase.app.discord.set_presence(
|
||||
state=self.getname(),
|
||||
details=f"Playing on {mapname}",
|
||||
large_image_key=preview,
|
||||
large_image_text=mapname,
|
||||
small_image_key=(
|
||||
babase.app.classic.platform if babase.app.classic else None
|
||||
),
|
||||
small_image_text=(
|
||||
babase.app.classic.platform if babase.app.classic else None
|
||||
),
|
||||
start_timestamp=int(time.time()),
|
||||
)
|
||||
|
||||
# Give our map a chance to override the music
|
||||
map_music = self._map_type.get_music_type()
|
||||
music = map_music if map_music is not None else self.default_music
|
||||
|
||||
|
|
@ -353,8 +374,14 @@ class GameActivity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team](
|
|||
if babase.app.classic is not None:
|
||||
babase.app.classic.game_begin_analytics()
|
||||
|
||||
# We don't do this in on_transition_in because it may depend on
|
||||
# players/teams which aren't available until now.
|
||||
# Update Discord party info
|
||||
if babase.app.discord.is_ready:
|
||||
party_size = len(self.players)
|
||||
max_size = max(8, party_size)
|
||||
babase.app.discord.set_presence(
|
||||
party_id=str(uuid.uuid4()), party_size=(party_size, max_size)
|
||||
)
|
||||
|
||||
_bascenev1.timer(0.001, self._show_scoreboard_info)
|
||||
_bascenev1.timer(1.0, self._show_info)
|
||||
_bascenev1.timer(2.5, self._show_tip)
|
||||
|
|
@ -1024,13 +1051,13 @@ class GameActivity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team](
|
|||
'text',
|
||||
attrs={
|
||||
'v_attach': 'bottom',
|
||||
'h_attach': 'left',
|
||||
'h_attach': 'right',
|
||||
'h_align': 'center',
|
||||
'v_align': 'center',
|
||||
'vr_depth': 300,
|
||||
'maxwidth': 100,
|
||||
'color': (1.0, 1.0, 1.0, 0.5),
|
||||
'position': (60, 50),
|
||||
'position': (-60, 50),
|
||||
'flatness': 1.0,
|
||||
'scale': 0.5,
|
||||
'text': babase.Lstr(resource='tournamentText'),
|
||||
|
|
@ -1042,13 +1069,13 @@ class GameActivity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team](
|
|||
'text',
|
||||
attrs={
|
||||
'v_attach': 'bottom',
|
||||
'h_attach': 'left',
|
||||
'h_attach': 'right',
|
||||
'h_align': 'center',
|
||||
'v_align': 'center',
|
||||
'vr_depth': 300,
|
||||
'maxwidth': 100,
|
||||
'color': (1.0, 1.0, 1.0, 0.5),
|
||||
'position': (60, 30),
|
||||
'position': (-60, 30),
|
||||
'flatness': 1.0,
|
||||
'scale': 0.9,
|
||||
},
|
||||
|
|
@ -1082,8 +1109,8 @@ class GameActivity[PlayerT: bascenev1.Player, TeamT: bascenev1.Team](
|
|||
assert self._tournament_time_limit_text.node
|
||||
self._tournament_time_limit_title_text.node.scale = 1.0
|
||||
self._tournament_time_limit_text.node.scale = 1.3
|
||||
self._tournament_time_limit_title_text.node.position = (80, 85)
|
||||
self._tournament_time_limit_text.node.position = (80, 60)
|
||||
self._tournament_time_limit_title_text.node.position = (-80, 85)
|
||||
self._tournament_time_limit_text.node.position = (-80, 60)
|
||||
cnode = _bascenev1.newnode(
|
||||
'combine',
|
||||
owner=self._tournament_time_limit_text.node,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue