Exposing networking sources

This commit is contained in:
Eric Froemling 2022-05-30 16:48:35 -07:00
parent 8ddaf71bf8
commit f0e1d7bdde
No known key found for this signature in database
GPG Key ID: 89C93F0F8D6D5A98
24 changed files with 4687 additions and 491 deletions

View File

@ -1,425 +1,425 @@
{
"assets/build/ba_data/audio/achievement.ogg": "https://files.ballistica.net/cache/ba1/d2/9f/8ba4d5c258e9841dde0235d794b5",
"assets/build/ba_data/audio/actionHero1.ogg": "https://files.ballistica.net/cache/ba1/a9/8e/717d49c51bba6b32d3d09f0d0f2b",
"assets/build/ba_data/audio/actionHero2.ogg": "https://files.ballistica.net/cache/ba1/ab/c7/70d6c42e89e584e29d8e6f70638e",
"assets/build/ba_data/audio/actionHero3.ogg": "https://files.ballistica.net/cache/ba1/27/d5/f549caba1da9969e7f2c15fc042e",
"assets/build/ba_data/audio/actionHero4.ogg": "https://files.ballistica.net/cache/ba1/8e/46/24e5ea7c5d80d4f95c487996a601",
"assets/build/ba_data/audio/actionHeroDeath.ogg": "https://files.ballistica.net/cache/ba1/a6/4a/6ed4571f79bd42dd2a97df6a6aaa",
"assets/build/ba_data/audio/actionHeroFall.ogg": "https://files.ballistica.net/cache/ba1/48/19/7e6743d5e4c68b74445fc1462c6c",
"assets/build/ba_data/audio/actionHeroHit1.ogg": "https://files.ballistica.net/cache/ba1/3c/45/ef2b2e8e033960fdc2ebc6378cb7",
"assets/build/ba_data/audio/actionHeroHit2.ogg": "https://files.ballistica.net/cache/ba1/8d/ff/27b2227b0b416005cf1ad0640316",
"assets/build/ba_data/audio/activateBeep.ogg": "https://files.ballistica.net/cache/ba1/e3/7c/53bc57c5c7f57bb35d22bb7f696a",
"assets/build/ba_data/audio/agent1.ogg": "https://files.ballistica.net/cache/ba1/52/9e/f41b7d3434bb99f711fef71ba8c6",
"assets/build/ba_data/audio/agent2.ogg": "https://files.ballistica.net/cache/ba1/3f/df/8ad978342caf7d4888d548a25acb",
"assets/build/ba_data/audio/agent3.ogg": "https://files.ballistica.net/cache/ba1/82/ef/00a7f193a6b884926d2fe1e3c1ed",
"assets/build/ba_data/audio/agent4.ogg": "https://files.ballistica.net/cache/ba1/e5/69/f39dcf5457f635e2253152c71939",
"assets/build/ba_data/audio/agentDeath.ogg": "https://files.ballistica.net/cache/ba1/20/36/ea6eab558ed786afadec5ccc5313",
"assets/build/ba_data/audio/agentFall.ogg": "https://files.ballistica.net/cache/ba1/ba/40/7584e852d068e68120d0d9174886",
"assets/build/ba_data/audio/agentHit1.ogg": "https://files.ballistica.net/cache/ba1/69/90/bcf822ca886392162aa22f4dde91",
"assets/build/ba_data/audio/agentHit2.ogg": "https://files.ballistica.net/cache/ba1/fa/81/17dd543d7f279158f808350403a3",
"assets/build/ba_data/audio/alarm.ogg": "https://files.ballistica.net/cache/ba1/1f/9f/a85fbb506297c6bff299cfca7220",
"assets/build/ba_data/audio/ali1.ogg": "https://files.ballistica.net/cache/ba1/3f/01/449837b714e5b8f7432eaa608fda",
"assets/build/ba_data/audio/ali2.ogg": "https://files.ballistica.net/cache/ba1/69/ef/064006961b26d1364f5f12174de8",
"assets/build/ba_data/audio/ali3.ogg": "https://files.ballistica.net/cache/ba1/08/0d/a124f81887ff8a7b8165cbf37af8",
"assets/build/ba_data/audio/ali4.ogg": "https://files.ballistica.net/cache/ba1/4f/9f/af5ee5de7d7616e2a095464b3410",
"assets/build/ba_data/audio/aliDeath.ogg": "https://files.ballistica.net/cache/ba1/c9/97/d67f9f9958569253ded3cc9f680c",
"assets/build/ba_data/audio/aliFall.ogg": "https://files.ballistica.net/cache/ba1/cc/09/af7da183c4fbebb066e0ab7dff0f",
"assets/build/ba_data/audio/aliHit1.ogg": "https://files.ballistica.net/cache/ba1/38/bf/f2e585574d65b8e9296655854c29",
"assets/build/ba_data/audio/aliHit2.ogg": "https://files.ballistica.net/cache/ba1/90/17/d1b1bf6c5430ef90fac3412d670a",
"assets/build/ba_data/audio/alien1.ogg": "https://files.ballistica.net/cache/ba1/d6/84/a7e374fe5e96b4c3b8d570b9887b",
"assets/build/ba_data/audio/alien2.ogg": "https://files.ballistica.net/cache/ba1/bd/4e/8ff64e144bd1b1bf8235950b8c0b",
"assets/build/ba_data/audio/alien3.ogg": "https://files.ballistica.net/cache/ba1/f5/e1/3f028e12a15b4acfbf7103785243",
"assets/build/ba_data/audio/alien4.ogg": "https://files.ballistica.net/cache/ba1/ef/a3/26359db5c116a8ebd4a34e15b3fb",
"assets/build/ba_data/audio/alienDeath.ogg": "https://files.ballistica.net/cache/ba1/ef/e3/cb997c5fc3f053f4255c0f416ba9",
"assets/build/ba_data/audio/alienFall.ogg": "https://files.ballistica.net/cache/ba1/1b/ca/ed192f6dcb92330e71c93ba196b6",
"assets/build/ba_data/audio/alienHit1.ogg": "https://files.ballistica.net/cache/ba1/d3/07/7d7402fa5d072829e2172b413d20",
"assets/build/ba_data/audio/alienHit2.ogg": "https://files.ballistica.net/cache/ba1/82/98/5f941cf1bb05e5bcbc7608c587ac",
"assets/build/ba_data/audio/announceEight.ogg": "https://files.ballistica.net/cache/ba1/a2/e0/a61ebd8009032c3dfcd7c68a40d9",
"assets/build/ba_data/audio/announceFive.ogg": "https://files.ballistica.net/cache/ba1/a7/9a/1886861099d6daca53e9fca3f572",
"assets/build/ba_data/audio/announceFour.ogg": "https://files.ballistica.net/cache/ba1/48/16/ace4874f8babda31cecf3ff59460",
"assets/build/ba_data/audio/announceNine.ogg": "https://files.ballistica.net/cache/ba1/4f/c2/66ae23dea5ad0dce1e3a4e028da3",
"assets/build/ba_data/audio/announceOne.ogg": "https://files.ballistica.net/cache/ba1/21/89/e0953e2951346753f5b049c3878d",
"assets/build/ba_data/audio/announceSeven.ogg": "https://files.ballistica.net/cache/ba1/cf/cf/9c26d68440bd704945cf83a42e3f",
"assets/build/ba_data/audio/announceSix.ogg": "https://files.ballistica.net/cache/ba1/98/a4/ab780d080a902cef3e261770670e",
"assets/build/ba_data/audio/announceTen.ogg": "https://files.ballistica.net/cache/ba1/df/cb/d68c7897013a1d9e03a46e20593f",
"assets/build/ba_data/audio/announceThree.ogg": "https://files.ballistica.net/cache/ba1/d3/21/e9b72b1b217bee2a83ebf3de802e",
"assets/build/ba_data/audio/announceTwo.ogg": "https://files.ballistica.net/cache/ba1/90/d4/baa71dc6c16de135266de4417ad8",
"assets/build/ba_data/audio/assassin1.ogg": "https://files.ballistica.net/cache/ba1/78/c1/9f7e31c9d5fd20a1a0e58946721e",
"assets/build/ba_data/audio/assassin2.ogg": "https://files.ballistica.net/cache/ba1/1d/6b/dd593f245bc766f67801c07c800b",
"assets/build/ba_data/audio/assassin3.ogg": "https://files.ballistica.net/cache/ba1/a9/86/90ced874739d1320608fe33f696c",
"assets/build/ba_data/audio/assassin4.ogg": "https://files.ballistica.net/cache/ba1/84/8e/4709c67341da6877f7531d87e990",
"assets/build/ba_data/audio/assassinDeath.ogg": "https://files.ballistica.net/cache/ba1/31/ee/dbaeb702cb04575b8262b4a01dcc",
"assets/build/ba_data/audio/assassinFall.ogg": "https://files.ballistica.net/cache/ba1/49/c1/316397d0c26f5fc423bbbbd27ff0",
"assets/build/ba_data/audio/assassinHit1.ogg": "https://files.ballistica.net/cache/ba1/27/ff/1a300e244dde1a9087df88c39bb2",
"assets/build/ba_data/audio/assassinHit2.ogg": "https://files.ballistica.net/cache/ba1/49/f6/4d49e96bb9fbdbabca92a5d84921",
"assets/build/ba_data/audio/bear1.ogg": "https://files.ballistica.net/cache/ba1/08/da/68f8e152d186ac93c35e356d22bd",
"assets/build/ba_data/audio/bear2.ogg": "https://files.ballistica.net/cache/ba1/6b/8d/edc1543e80e290aa670bf7474885",
"assets/build/ba_data/audio/bear3.ogg": "https://files.ballistica.net/cache/ba1/34/6d/09656673977df46e96062cba9b8a",
"assets/build/ba_data/audio/bear4.ogg": "https://files.ballistica.net/cache/ba1/18/b6/3d7d28a8b25af86b33aa92f02a78",
"assets/build/ba_data/audio/bearDeath.ogg": "https://files.ballistica.net/cache/ba1/d6/4d/42a0a7f23b207095ca6d2e3fb415",
"assets/build/ba_data/audio/bearFall.ogg": "https://files.ballistica.net/cache/ba1/0a/f9/48efaf15321f5ab04353dbcefe3a",
"assets/build/ba_data/audio/bearHit1.ogg": "https://files.ballistica.net/cache/ba1/dc/dd/a59124d4ee4f068ce87dfad96a63",
"assets/build/ba_data/audio/bearHit2.ogg": "https://files.ballistica.net/cache/ba1/ef/c7/2fc7cbdcf4625b7015e050958eeb",
"assets/build/ba_data/audio/bellHigh.ogg": "https://files.ballistica.net/cache/ba1/4a/e2/1842c6aa68a0fc8db5713eb1f4d1",
"assets/build/ba_data/audio/bellLow.ogg": "https://files.ballistica.net/cache/ba1/70/a5/27615f255641f9bf97ba02ee610f",
"assets/build/ba_data/audio/bellMed.ogg": "https://files.ballistica.net/cache/ba1/b9/93/5a847d9d8476f64fa6e3a54e49e3",
"assets/build/ba_data/audio/bigImpact.ogg": "https://files.ballistica.net/cache/ba1/9e/6d/d386b11c201c6942eff44b2f902b",
"assets/build/ba_data/audio/bigImpact2.ogg": "https://files.ballistica.net/cache/ba1/ca/d7/7b1a99160176f4f984b40dc88c5c",
"assets/build/ba_data/audio/blank.ogg": "https://files.ballistica.net/cache/ba1/a4/bb/d564bc6b2bbce15e9413835faee3",
"assets/build/ba_data/audio/blip.ogg": "https://files.ballistica.net/cache/ba1/25/05/bc35e1834a8d3b6fb098b70ad559",
"assets/build/ba_data/audio/block.ogg": "https://files.ballistica.net/cache/ba1/1c/7d/aa6d3454431c5a6a1c9d44ee3dda",
"assets/build/ba_data/audio/bombDrop01.ogg": "https://files.ballistica.net/cache/ba1/10/2f/0ec5e8335e1c522d77a877808d4d",
"assets/build/ba_data/audio/bombDrop02.ogg": "https://files.ballistica.net/cache/ba1/82/d8/c5879ccf65062410d30e388ed550",
"assets/build/ba_data/audio/bombRoll01.ogg": "https://files.ballistica.net/cache/ba1/d2/92/526900582d9d19fa730d0f23f31d",
"assets/build/ba_data/audio/bones1.ogg": "https://files.ballistica.net/cache/ba1/c7/fd/3654fc5875828592aa2240c1a5c4",
"assets/build/ba_data/audio/bones2.ogg": "https://files.ballistica.net/cache/ba1/c0/bc/e2f489ed7fa5b8f29f4214f92a9b",
"assets/build/ba_data/audio/bones3.ogg": "https://files.ballistica.net/cache/ba1/ac/f0/67262750370df9ce2fa0ce6cf084",
"assets/build/ba_data/audio/bonesDeath.ogg": "https://files.ballistica.net/cache/ba1/19/3c/31d70812dc519d32b124602424e0",
"assets/build/ba_data/audio/bonesFall.ogg": "https://files.ballistica.net/cache/ba1/85/a7/a2231b604aa73e4f72ba93a709aa",
"assets/build/ba_data/audio/boo.ogg": "https://files.ballistica.net/cache/ba1/4f/61/810cac6c4dc04ce0e484b5f0ee95",
"assets/build/ba_data/audio/boxDrop.ogg": "https://files.ballistica.net/cache/ba1/88/73/727941acfef7a14f8448bb50c51f",
"assets/build/ba_data/audio/boxingBell.ogg": "https://files.ballistica.net/cache/ba1/a9/66/ae2645c9fd64011dde6f96c9b3ad",
"assets/build/ba_data/audio/bunny1.ogg": "https://files.ballistica.net/cache/ba1/20/a8/7fe62f74802a4a4fd13385440d0c",
"assets/build/ba_data/audio/bunny2.ogg": "https://files.ballistica.net/cache/ba1/0f/ea/af1163da28dd803799460d0cf75e",
"assets/build/ba_data/audio/bunny3.ogg": "https://files.ballistica.net/cache/ba1/f4/a6/c5bc1fe3b25108635025290b9c41",
"assets/build/ba_data/audio/bunny4.ogg": "https://files.ballistica.net/cache/ba1/00/4f/77facdcc2b2ba8fbf1a20cf3b273",
"assets/build/ba_data/audio/bunnyDeath.ogg": "https://files.ballistica.net/cache/ba1/d7/40/07e61c803b187abd0715c0e226de",
"assets/build/ba_data/audio/bunnyFall.ogg": "https://files.ballistica.net/cache/ba1/c9/c1/c15c0e6c8dfde62315a8c5314d30",
"assets/build/ba_data/audio/bunnyHit1.ogg": "https://files.ballistica.net/cache/ba1/4a/7f/903703552b19e71693ec041037ac",
"assets/build/ba_data/audio/bunnyHit2.ogg": "https://files.ballistica.net/cache/ba1/d0/31/cb5fd87a73ce47b9c79f87e163ab",
"assets/build/ba_data/audio/bunnyJump.ogg": "https://files.ballistica.net/cache/ba1/13/04/e46e6c1d7644606885e4b43800b2",
"assets/build/ba_data/audio/cashRegister.ogg": "https://files.ballistica.net/cache/ba1/8e/9f/8e03065e97edeaa59703120a0ed4",
"assets/build/ba_data/audio/cashRegister2.ogg": "https://files.ballistica.net/cache/ba1/2b/17/39e8e9bda873e09db8ab599ebd70",
"assets/build/ba_data/audio/charSelectMusic.ogg": "https://files.ballistica.net/cache/ba1/a5/e5/deae67f63b9be8eca28169908ce3",
"assets/build/ba_data/audio/cheer.ogg": "https://files.ballistica.net/cache/ba1/87/5b/7982e82c6a09d2e6f351ef665288",
"assets/build/ba_data/audio/click01.ogg": "https://files.ballistica.net/cache/ba1/06/2a/e91c1eb70b729731a825647cbbfb",
"assets/build/ba_data/audio/corkPop.ogg": "https://files.ballistica.net/cache/ba1/33/5f/58da365edf74e1b981b59891ddb4",
"assets/build/ba_data/audio/cowboy1.ogg": "https://files.ballistica.net/cache/ba1/a7/05/680daba00b737cb918eec632dbfe",
"assets/build/ba_data/audio/cowboy2.ogg": "https://files.ballistica.net/cache/ba1/2b/67/15711076f135fa638bd072a4b487",
"assets/build/ba_data/audio/cowboy3.ogg": "https://files.ballistica.net/cache/ba1/4b/0f/46de2edd7a264fba3a8973c078ca",
"assets/build/ba_data/audio/cowboy4.ogg": "https://files.ballistica.net/cache/ba1/1e/78/f8b4d4d9fd1bdcce9b79fa6f3d0c",
"assets/build/ba_data/audio/cowboyDeath.ogg": "https://files.ballistica.net/cache/ba1/05/1a/afae164d0e81babbacaa0eed27ea",
"assets/build/ba_data/audio/cowboyFall.ogg": "https://files.ballistica.net/cache/ba1/57/2f/a9c6adbe96fa94ab840be5373a44",
"assets/build/ba_data/audio/cowboyHit1.ogg": "https://files.ballistica.net/cache/ba1/9e/f7/4dadf69fb9e04a08871c5967a05c",
"assets/build/ba_data/audio/cowboyHit2.ogg": "https://files.ballistica.net/cache/ba1/d9/3c/4a4507cf25a5bf2f6fad76e6a9e1",
"assets/build/ba_data/audio/crowdChant.ogg": "https://files.ballistica.net/cache/ba1/36/db/f4432f85f19cce87dc8a62ba497d",
"assets/build/ba_data/audio/cyborg1.ogg": "https://files.ballistica.net/cache/ba1/6c/62/33a5b99062293b0a38cdf484bc58",
"assets/build/ba_data/audio/cyborg2.ogg": "https://files.ballistica.net/cache/ba1/b7/a3/032be02fca5b2b06e3f7d6500640",
"assets/build/ba_data/audio/cyborg3.ogg": "https://files.ballistica.net/cache/ba1/c3/82/1e7949473dbbdaa9688f11ad282a",
"assets/build/ba_data/audio/cyborg4.ogg": "https://files.ballistica.net/cache/ba1/7c/d6/2461bd28a2c40be389f3f5c5b517",
"assets/build/ba_data/audio/cyborgDeath.ogg": "https://files.ballistica.net/cache/ba1/d8/47/3376feef35a36db9c31da212a911",
"assets/build/ba_data/audio/cyborgFall.ogg": "https://files.ballistica.net/cache/ba1/2c/87/4c812fe231fcd3b77a1b63b5ae8d",
"assets/build/ba_data/audio/cyborgHit1.ogg": "https://files.ballistica.net/cache/ba1/e0/7f/65f0ceab26d2ec41901a7dba3367",
"assets/build/ba_data/audio/cyborgHit2.ogg": "https://files.ballistica.net/cache/ba1/92/8e/2735ab2817ab899c810faa3f4214",
"assets/build/ba_data/audio/cymbal.ogg": "https://files.ballistica.net/cache/ba1/47/bb/89f7104e8b1b58844f2b7bbea5c8",
"assets/build/ba_data/audio/debrisFall.ogg": "https://files.ballistica.net/cache/ba1/59/f8/793797d16965601403fbd76d7122",
"assets/build/ba_data/audio/deek.ogg": "https://files.ballistica.net/cache/ba1/4f/5d/2b8d74c023bd92db32a9b6abf27e",
"assets/build/ba_data/audio/deek2.ogg": "https://files.ballistica.net/cache/ba1/31/3e/ecc2351f4097de01b4ef19d8e4a1",
"assets/build/ba_data/audio/ding.ogg": "https://files.ballistica.net/cache/ba1/9f/4f/0246b0dc02d11a47a866006d364b",
"assets/build/ba_data/audio/dingSmall.ogg": "https://files.ballistica.net/cache/ba1/a8/f3/02a413d75105c4e789d6c62d983f",
"assets/build/ba_data/audio/dingSmallHigh.ogg": "https://files.ballistica.net/cache/ba1/f8/f8/fb3490eda7ca3daaff63e0415830",
"assets/build/ba_data/audio/dripity.ogg": "https://files.ballistica.net/cache/ba1/a1/33/f9189cdf3088e37b7af0f2da9f6c",
"assets/build/ba_data/audio/drumRoll.ogg": "https://files.ballistica.net/cache/ba1/e8/13/086773cd523943940cf3de806497",
"assets/build/ba_data/audio/error.ogg": "https://files.ballistica.net/cache/ba1/b4/ad/9dfe09980f2d6c21af2e40e3ca87",
"assets/build/ba_data/audio/explosion01.ogg": "https://files.ballistica.net/cache/ba1/2f/d9/97b803aa445fee5282f3a8bcdd5a",
"assets/build/ba_data/audio/explosion02.ogg": "https://files.ballistica.net/cache/ba1/87/88/996566f228d9473956c8c2575003",
"assets/build/ba_data/audio/explosion03.ogg": "https://files.ballistica.net/cache/ba1/0c/d8/68d1776fedbb89620f9d7615f634",
"assets/build/ba_data/audio/explosion04.ogg": "https://files.ballistica.net/cache/ba1/89/c5/70e32b94138e11275ea222882840",
"assets/build/ba_data/audio/explosion05.ogg": "https://files.ballistica.net/cache/ba1/01/08/43231dbc2c3630da84c446c3b57e",
"assets/build/ba_data/audio/fanfare.ogg": "https://files.ballistica.net/cache/ba1/16/d0/8b7d5c981390d0b71eac730a810d",
"assets/build/ba_data/audio/flagCatcherMusic.ogg": "https://files.ballistica.net/cache/ba1/86/e7/0e647b76c4d28b788fd80e0e2546",
"assets/build/ba_data/audio/flyingMusic.ogg": "https://files.ballistica.net/cache/ba1/95/cd/61d54bc3e331f0b2ec4a7dd55e37",
"assets/build/ba_data/audio/foghorn.ogg": "https://files.ballistica.net/cache/ba1/17/6b/58c0f2aae49f9e3dcb845a3d45f4",
"assets/build/ba_data/audio/footImpact01.ogg": "https://files.ballistica.net/cache/ba1/21/23/41148f46251d9fe740a9f6975bb7",
"assets/build/ba_data/audio/footImpact02.ogg": "https://files.ballistica.net/cache/ba1/15/0a/1bc32d46e2c5564d423c4eef2594",
"assets/build/ba_data/audio/footImpact03.ogg": "https://files.ballistica.net/cache/ba1/13/da/5fc81a71dfd3cf9570d5a31289e1",
"assets/build/ba_data/audio/forwardMarchMusic.ogg": "https://files.ballistica.net/cache/ba1/35/04/e6f29d4167b0cffed6cd95b4edaa",
"assets/build/ba_data/audio/freeze.ogg": "https://files.ballistica.net/cache/ba1/7c/63/c7ffc0a3748acfe48d15299433b2",
"assets/build/ba_data/audio/frosty01.ogg": "https://files.ballistica.net/cache/ba1/ca/f3/fbb572d011011773ef9e2cb8d517",
"assets/build/ba_data/audio/frosty02.ogg": "https://files.ballistica.net/cache/ba1/62/b0/31dd9e9adb86006e7cc22bcc43d5",
"assets/build/ba_data/audio/frosty03.ogg": "https://files.ballistica.net/cache/ba1/6d/81/bdad0ab14f5d08e4eec07b448820",
"assets/build/ba_data/audio/frosty04.ogg": "https://files.ballistica.net/cache/ba1/62/8a/b24b5b34ee4353a48f7a80e7d526",
"assets/build/ba_data/audio/frosty05.ogg": "https://files.ballistica.net/cache/ba1/e2/d8/fb46e471d4c18269e2b463d8a4d9",
"assets/build/ba_data/audio/frostyDeath.ogg": "https://files.ballistica.net/cache/ba1/47/1b/1c646af3b2c17ba3daf7092a2c72",
"assets/build/ba_data/audio/frostyFall.ogg": "https://files.ballistica.net/cache/ba1/70/1e/4276253d747e380a2de9ce4514f6",
"assets/build/ba_data/audio/frostyHit01.ogg": "https://files.ballistica.net/cache/ba1/2a/f6/6e2df24f49af739fedb1f7f65a6e",
"assets/build/ba_data/audio/frostyHit02.ogg": "https://files.ballistica.net/cache/ba1/ac/26/ef3f46f8ed9d65225c32468d71c3",
"assets/build/ba_data/audio/frostyHit03.ogg": "https://files.ballistica.net/cache/ba1/c1/ba/89d48c268110acec203e7f9f6fd9",
"assets/build/ba_data/audio/fuse01.ogg": "https://files.ballistica.net/cache/ba1/d9/ea/0e00da95a2f1e611ddb07211ebe5",
"assets/build/ba_data/audio/gladiator1.ogg": "https://files.ballistica.net/cache/ba1/d7/39/bd8119eae6558cc58d2024e9fd27",
"assets/build/ba_data/audio/gladiator2.ogg": "https://files.ballistica.net/cache/ba1/71/45/f3f027adb94077308cb1cf80e05f",
"assets/build/ba_data/audio/gladiator3.ogg": "https://files.ballistica.net/cache/ba1/52/95/231c534a606bd9df2cd94a0261b0",
"assets/build/ba_data/audio/gladiator4.ogg": "https://files.ballistica.net/cache/ba1/39/0d/17e8d36c933bc2476ddb45f1f1f8",
"assets/build/ba_data/audio/gladiatorDeath.ogg": "https://files.ballistica.net/cache/ba1/54/e8/9489e9cb83a6509258e53140fb3d",
"assets/build/ba_data/audio/gladiatorFall.ogg": "https://files.ballistica.net/cache/ba1/a5/17/ba3cdd74be5110fdaa36c9ba9f23",
"assets/build/ba_data/audio/gladiatorHit1.ogg": "https://files.ballistica.net/cache/ba1/bf/01/1f7363688b77a9b522c0cfb24d32",
"assets/build/ba_data/audio/gladiatorHit2.ogg": "https://files.ballistica.net/cache/ba1/dc/10/88a3664f6d53e0509d2393d88aa1",
"assets/build/ba_data/audio/gong.ogg": "https://files.ballistica.net/cache/ba1/4a/93/5f144784791eb6a9556c1608f449",
"assets/build/ba_data/audio/grandRompMusic.ogg": "https://files.ballistica.net/cache/ba1/fe/37/2444874e5fa5ca5b2006b1e02d5b",
"assets/build/ba_data/audio/gravelSkid.ogg": "https://files.ballistica.net/cache/ba1/69/c3/f342f0728fca01f5815bebc12b5e",
"assets/build/ba_data/audio/gunCocking.ogg": "https://files.ballistica.net/cache/ba1/ad/5d/61299a3e7e749b0820b3b14e85f8",
"assets/build/ba_data/audio/healthPowerup.ogg": "https://files.ballistica.net/cache/ba1/b7/f8/325609f9aeefc7c7625ee9b6fdc2",
"assets/build/ba_data/audio/hiss.ogg": "https://files.ballistica.net/cache/ba1/80/38/767dbf6d2be69784de415bd3242c",
"assets/build/ba_data/audio/impactHard.ogg": "https://files.ballistica.net/cache/ba1/da/02/d9eaa62bbb6cd2f945be58079e67",
"assets/build/ba_data/audio/impactHard2.ogg": "https://files.ballistica.net/cache/ba1/5f/20/e755ece22d2a9830855a4e1f6070",
"assets/build/ba_data/audio/impactHard3.ogg": "https://files.ballistica.net/cache/ba1/fa/96/4ad8be51f4e5be4a7816cac306d5",
"assets/build/ba_data/audio/impactMedium.ogg": "https://files.ballistica.net/cache/ba1/71/c2/a515223089fd7869557ff77e8fb3",
"assets/build/ba_data/audio/impactMedium2.ogg": "https://files.ballistica.net/cache/ba1/86/69/13a025f692a55fcbfee563ed863b",
"assets/build/ba_data/audio/jack01.ogg": "https://files.ballistica.net/cache/ba1/27/12/fcdce5c2534201b15ca4c2380608",
"assets/build/ba_data/audio/jack02.ogg": "https://files.ballistica.net/cache/ba1/33/5f/6bb0dffec14c2d3a3c601da23377",
"assets/build/ba_data/audio/jack03.ogg": "https://files.ballistica.net/cache/ba1/08/e3/ae5342d0fd19b90d8ae688b45fdd",
"assets/build/ba_data/audio/jack04.ogg": "https://files.ballistica.net/cache/ba1/fe/d4/b264d6ada9ada3bfdef372392288",
"assets/build/ba_data/audio/jack05.ogg": "https://files.ballistica.net/cache/ba1/a3/5f/45a42149edcad5d9e62a4dd8ad93",
"assets/build/ba_data/audio/jack06.ogg": "https://files.ballistica.net/cache/ba1/d0/0f/c7e91e47e838b08770c5de97246b",
"assets/build/ba_data/audio/jackDeath01.ogg": "https://files.ballistica.net/cache/ba1/e2/b8/02e58d71ae249544615a67c03296",
"assets/build/ba_data/audio/jackFall01.ogg": "https://files.ballistica.net/cache/ba1/2e/13/860f88fa2b8c4b1defa6a5fceb0a",
"assets/build/ba_data/audio/jackHit01.ogg": "https://files.ballistica.net/cache/ba1/b9/6b/33fffdd01b8bdccaf86855f2adbf",
"assets/build/ba_data/audio/jackHit02.ogg": "https://files.ballistica.net/cache/ba1/94/23/3774cac4aa2b6686744595e7bb5c",
"assets/build/ba_data/audio/jackHit03.ogg": "https://files.ballistica.net/cache/ba1/95/58/b37b545f984225c8b9d8c100c1ad",
"assets/build/ba_data/audio/jackHit04.ogg": "https://files.ballistica.net/cache/ba1/33/a2/923ee4faf663ffdcb7c4915408d7",
"assets/build/ba_data/audio/jackHit05.ogg": "https://files.ballistica.net/cache/ba1/91/bf/8acb25409d688f8868732a98869e",
"assets/build/ba_data/audio/jackHit06.ogg": "https://files.ballistica.net/cache/ba1/f1/29/079b85a50ac3227eb7580a3bcca0",
"assets/build/ba_data/audio/jackHit07.ogg": "https://files.ballistica.net/cache/ba1/a6/81/39bf1cd3f6829af9b14b20225907",
"assets/build/ba_data/audio/jumpsuit1.ogg": "https://files.ballistica.net/cache/ba1/bd/77/58cc735320dd9c8cc42f607d4b5d",
"assets/build/ba_data/audio/jumpsuit2.ogg": "https://files.ballistica.net/cache/ba1/e9/b4/a20e16879bd9dbb799a234b765d5",
"assets/build/ba_data/audio/jumpsuit3.ogg": "https://files.ballistica.net/cache/ba1/a0/c6/f7fa993a5fd278a78b46e09f9b75",
"assets/build/ba_data/audio/jumpsuit4.ogg": "https://files.ballistica.net/cache/ba1/a1/27/4da6a33575dbe88648dfbc8d05d7",
"assets/build/ba_data/audio/jumpsuitDeath.ogg": "https://files.ballistica.net/cache/ba1/80/8e/42fe43c6e622b03172ba9189d5f1",
"assets/build/ba_data/audio/jumpsuitFall.ogg": "https://files.ballistica.net/cache/ba1/9e/c2/05090792c71d651017d7d4a15850",
"assets/build/ba_data/audio/jumpsuitHit1.ogg": "https://files.ballistica.net/cache/ba1/74/14/4266a79b4a1b1f61fdc6a5c65fbe",
"assets/build/ba_data/audio/jumpsuitHit2.ogg": "https://files.ballistica.net/cache/ba1/32/1f/8dc55112a2cbba1921b5d6f89ac3",
"assets/build/ba_data/audio/kronk1.ogg": "https://files.ballistica.net/cache/ba1/4e/6f/9f55ffe6b9739371a0a810a600e5",
"assets/build/ba_data/audio/kronk10.ogg": "https://files.ballistica.net/cache/ba1/47/f8/51bebed376036961c88565c06c14",
"assets/build/ba_data/audio/kronk2.ogg": "https://files.ballistica.net/cache/ba1/09/08/a0bec68a4112cf585677afe748a4",
"assets/build/ba_data/audio/kronk3.ogg": "https://files.ballistica.net/cache/ba1/8e/c8/2ed9ae22042b9acf0feeec8ae1ef",
"assets/build/ba_data/audio/kronk4.ogg": "https://files.ballistica.net/cache/ba1/da/b1/57b26205ef88c824820d74d06453",
"assets/build/ba_data/audio/kronk5.ogg": "https://files.ballistica.net/cache/ba1/b2/67/a3d3ff13521f9d580c68d59b45be",
"assets/build/ba_data/audio/kronk6.ogg": "https://files.ballistica.net/cache/ba1/f5/43/1411aabd658621b66f85f0c48263",
"assets/build/ba_data/audio/kronk7.ogg": "https://files.ballistica.net/cache/ba1/31/3a/8d9c64badd6d6b5370d210348b4d",
"assets/build/ba_data/audio/kronk8.ogg": "https://files.ballistica.net/cache/ba1/a3/56/2e62b9e34892645d3229e8953508",
"assets/build/ba_data/audio/kronk9.ogg": "https://files.ballistica.net/cache/ba1/65/ef/dd4851e044c2edec0931c58261d1",
"assets/build/ba_data/audio/kronkDeath.ogg": "https://files.ballistica.net/cache/ba1/3b/59/8918fbbc563e2053a48708a1c5ca",
"assets/build/ba_data/audio/kronkFall.ogg": "https://files.ballistica.net/cache/ba1/04/d9/5c4eb21b88d943b39e2a338c93ca",
"assets/build/ba_data/audio/laser.ogg": "https://files.ballistica.net/cache/ba1/85/28/4870efb11aa77d4e628472ce2f62",
"assets/build/ba_data/audio/laserReverse.ogg": "https://files.ballistica.net/cache/ba1/98/ca/b1b23e0c21acf47876d677328493",
"assets/build/ba_data/audio/mel01.ogg": "https://files.ballistica.net/cache/ba1/2c/8f/36e020affbf23cbfa20dc486291e",
"assets/build/ba_data/audio/mel02.ogg": "https://files.ballistica.net/cache/ba1/bd/56/f5d03c1d70bb298f02ace96d0fe4",
"assets/build/ba_data/audio/mel03.ogg": "https://files.ballistica.net/cache/ba1/43/d6/92cf472e4fdcc62dd28d24617121",
"assets/build/ba_data/audio/mel04.ogg": "https://files.ballistica.net/cache/ba1/6f/4a/c729e3fa4f0bc04b9d0c86454ba0",
"assets/build/ba_data/audio/mel05.ogg": "https://files.ballistica.net/cache/ba1/38/d3/3b4b44646fb32c7616bbdb84943c",
"assets/build/ba_data/audio/mel06.ogg": "https://files.ballistica.net/cache/ba1/c1/f2/2209937359c488e2eee3ebfb0b74",
"assets/build/ba_data/audio/mel07.ogg": "https://files.ballistica.net/cache/ba1/c7/00/88715b50c463ec8f90aa5f5d814a",
"assets/build/ba_data/audio/mel08.ogg": "https://files.ballistica.net/cache/ba1/cc/18/51b007d3eda6cd03dd9ae6575161",
"assets/build/ba_data/audio/mel09.ogg": "https://files.ballistica.net/cache/ba1/44/aa/1edab3702912292f0ba5d25d6b29",
"assets/build/ba_data/audio/mel10.ogg": "https://files.ballistica.net/cache/ba1/1b/7b/2b8e2684f3b9f5eaec906d2c895b",
"assets/build/ba_data/audio/melDeath01.ogg": "https://files.ballistica.net/cache/ba1/4a/f8/cf3a2f5394d1a1350ce65574c217",
"assets/build/ba_data/audio/melFall01.ogg": "https://files.ballistica.net/cache/ba1/81/30/94cc749ec9b4f6e04d8ae0ff8891",
"assets/build/ba_data/audio/menuMusic.ogg": "https://files.ballistica.net/cache/ba1/bd/95/1c1d27c90cedcd3c2c466ba4af05",
"assets/build/ba_data/audio/metalHit.ogg": "https://files.ballistica.net/cache/ba1/f7/98/3d81888061e6821438c47987dda1",
"assets/build/ba_data/audio/metalSkid.ogg": "https://files.ballistica.net/cache/ba1/17/13/5029f01d8c048a58cfcbed0ddcc4",
"assets/build/ba_data/audio/ninjaAttack1.ogg": "https://files.ballistica.net/cache/ba1/2a/18/1f1ad3878d79efe97b408d745881",
"assets/build/ba_data/audio/ninjaAttack2.ogg": "https://files.ballistica.net/cache/ba1/b9/4d/3f93af21c7a073f0751e307dd4b0",
"assets/build/ba_data/audio/ninjaAttack3.ogg": "https://files.ballistica.net/cache/ba1/13/c3/8251857b304034ef93a9ec668381",
"assets/build/ba_data/audio/ninjaAttack4.ogg": "https://files.ballistica.net/cache/ba1/45/39/714130286da1a5edae376c6efd92",
"assets/build/ba_data/audio/ninjaAttack5.ogg": "https://files.ballistica.net/cache/ba1/ed/38/2cdcaea2bbbd84bc0fe1cb4f184a",
"assets/build/ba_data/audio/ninjaAttack6.ogg": "https://files.ballistica.net/cache/ba1/38/6a/df7417238bd97fc68afdc4a0d1dd",
"assets/build/ba_data/audio/ninjaAttack7.ogg": "https://files.ballistica.net/cache/ba1/56/f0/3b82da9e99b259c857bc269f1898",
"assets/build/ba_data/audio/ninjaDeath1.ogg": "https://files.ballistica.net/cache/ba1/f9/04/cadcebd5c3729926f2f50370a8e4",
"assets/build/ba_data/audio/ninjaFall1.ogg": "https://files.ballistica.net/cache/ba1/38/69/7179a0a5655f810f28b19af5cc1f",
"assets/build/ba_data/audio/ninjaHit1.ogg": "https://files.ballistica.net/cache/ba1/3d/5d/8e1fed6e0e40d7743613cdf5ef59",
"assets/build/ba_data/audio/ninjaHit2.ogg": "https://files.ballistica.net/cache/ba1/2c/ba/a75741ab5e5de95d7d8e88969a22",
"assets/build/ba_data/audio/ninjaHit3.ogg": "https://files.ballistica.net/cache/ba1/bf/5f/7265f22f04e2130c24f2a001d0c2",
"assets/build/ba_data/audio/ninjaHit4.ogg": "https://files.ballistica.net/cache/ba1/5f/b0/ede4fa97a9dfa27b2ca1107fee16",
"assets/build/ba_data/audio/ninjaHit5.ogg": "https://files.ballistica.net/cache/ba1/f0/00/ec3188103c119c1dc4781f28604b",
"assets/build/ba_data/audio/ninjaHit6.ogg": "https://files.ballistica.net/cache/ba1/69/34/e835f781dfa77da25124824d8aac",
"assets/build/ba_data/audio/ninjaHit7.ogg": "https://files.ballistica.net/cache/ba1/b0/b1/0934cf37dae8ebf9413cb619c89b",
"assets/build/ba_data/audio/ninjaHit8.ogg": "https://files.ballistica.net/cache/ba1/81/ea/c0b6ba9f05499d1254c80eaf56fd",
"assets/build/ba_data/audio/oldLady1.ogg": "https://files.ballistica.net/cache/ba1/88/8c/e63c26010c85d164adce3990f30e",
"assets/build/ba_data/audio/oldLady2.ogg": "https://files.ballistica.net/cache/ba1/96/af/84fa9302f076b6a6276bc99179e0",
"assets/build/ba_data/audio/oldLady3.ogg": "https://files.ballistica.net/cache/ba1/30/50/4d10bb83e747029460f7870e1601",
"assets/build/ba_data/audio/oldLady4.ogg": "https://files.ballistica.net/cache/ba1/60/f2/a38d94950cc14ee2fdfb31e00146",
"assets/build/ba_data/audio/oldLadyDeath.ogg": "https://files.ballistica.net/cache/ba1/09/2a/bbf7214449d1bbfbfdbac8cb11a9",
"assets/build/ba_data/audio/oldLadyFall.ogg": "https://files.ballistica.net/cache/ba1/63/6e/9758dc9487224d80129a17d9be04",
"assets/build/ba_data/audio/oldLadyHit1.ogg": "https://files.ballistica.net/cache/ba1/92/8d/c5a9d63b9bb4d72784ccbfb86bdf",
"assets/build/ba_data/audio/oldLadyHit2.ogg": "https://files.ballistica.net/cache/ba1/2f/ed/b0dc30ff4fbc5f024aa0cc86e6a2",
"assets/build/ba_data/audio/ooh.ogg": "https://files.ballistica.net/cache/ba1/ca/17/6a512761959489dd97cea66f8fb9",
"assets/build/ba_data/audio/operaSinger1.ogg": "https://files.ballistica.net/cache/ba1/1f/e6/5cbc1d640d779651abda6ddc2510",
"assets/build/ba_data/audio/operaSinger2.ogg": "https://files.ballistica.net/cache/ba1/85/82/b7fce504e3b3ab1e3a90c8ceca2a",
"assets/build/ba_data/audio/operaSinger3.ogg": "https://files.ballistica.net/cache/ba1/2c/3d/94c95cfb0ddbd2287aa98ff9ec52",
"assets/build/ba_data/audio/operaSinger4.ogg": "https://files.ballistica.net/cache/ba1/4e/ae/b918a8de6d54f072d3cd7f4d0c18",
"assets/build/ba_data/audio/operaSingerDeath.ogg": "https://files.ballistica.net/cache/ba1/4b/d5/46061ca14b67460493259045ea43",
"assets/build/ba_data/audio/operaSingerFall.ogg": "https://files.ballistica.net/cache/ba1/d8/9a/78d427405b693121c8cf18e789e1",
"assets/build/ba_data/audio/operaSingerHit1.ogg": "https://files.ballistica.net/cache/ba1/fb/16/007d8e2c229cbf1861021e0a0023",
"assets/build/ba_data/audio/operaSingerHit2.ogg": "https://files.ballistica.net/cache/ba1/8f/94/0041b1ba8cbc8155c1156a4a9d92",
"assets/build/ba_data/audio/orchestraHit.ogg": "https://files.ballistica.net/cache/ba1/34/39/230a50bafeb4fa7ed5b7edc73a36",
"assets/build/ba_data/audio/orchestraHit2.ogg": "https://files.ballistica.net/cache/ba1/08/1b/74e10d63ce851db3339352fa7d20",
"assets/build/ba_data/audio/orchestraHit3.ogg": "https://files.ballistica.net/cache/ba1/e1/97/bf98875661f61f733207b28d299a",
"assets/build/ba_data/audio/orchestraHit4.ogg": "https://files.ballistica.net/cache/ba1/7a/33/d13149b91c176002c51caacc0b04",
"assets/build/ba_data/audio/orchestraHitBig1.ogg": "https://files.ballistica.net/cache/ba1/1b/ad/10baf05c9348c34cd5c4333905b0",
"assets/build/ba_data/audio/orchestraHitBig2.ogg": "https://files.ballistica.net/cache/ba1/3e/9c/d7e6d61c713cc4bdee2116657eee",
"assets/build/ba_data/audio/penguin1.ogg": "https://files.ballistica.net/cache/ba1/02/52/3f09afe2ecfbf07501a93b43f3b1",
"assets/build/ba_data/audio/penguin2.ogg": "https://files.ballistica.net/cache/ba1/25/bf/632302bd31340b129f5945376530",
"assets/build/ba_data/audio/penguin3.ogg": "https://files.ballistica.net/cache/ba1/8c/20/a82230a6b246d5a46f91c791a3e7",
"assets/build/ba_data/audio/penguin4.ogg": "https://files.ballistica.net/cache/ba1/bb/ba/83754ebb41d4ef7b2c0fa8dd51ab",
"assets/build/ba_data/audio/penguinDeath.ogg": "https://files.ballistica.net/cache/ba1/fb/49/b8e0919f57a4eb021b1cb969a88a",
"assets/build/ba_data/audio/penguinFall.ogg": "https://files.ballistica.net/cache/ba1/1f/c3/e8f21b8b3657ffa8b86af8dacc6f",
"assets/build/ba_data/audio/penguinHit1.ogg": "https://files.ballistica.net/cache/ba1/15/d6/7ce277305546211e8e6aed59e5a0",
"assets/build/ba_data/audio/penguinHit2.ogg": "https://files.ballistica.net/cache/ba1/a3/34/e3cfa4803c4ae29788857eabb87d",
"assets/build/ba_data/audio/pixie1.ogg": "https://files.ballistica.net/cache/ba1/5d/e7/e3bb76e4855bab3bfa237975a8b6",
"assets/build/ba_data/audio/pixie2.ogg": "https://files.ballistica.net/cache/ba1/ab/39/2a73f2b59560267557d6aa9a1d94",
"assets/build/ba_data/audio/pixie3.ogg": "https://files.ballistica.net/cache/ba1/94/36/0e8edbfdcc0e327095c9153b7f1b",
"assets/build/ba_data/audio/pixie4.ogg": "https://files.ballistica.net/cache/ba1/95/ae/a33bdbdb81ad8e9739df5f96a43f",
"assets/build/ba_data/audio/pixieDeath.ogg": "https://files.ballistica.net/cache/ba1/5d/8c/22cab8ae2623d284b8c70dc08638",
"assets/build/ba_data/audio/pixieFall.ogg": "https://files.ballistica.net/cache/ba1/37/54/7c75f1660a9aa2bc5f370a1414eb",
"assets/build/ba_data/audio/pixieHit1.ogg": "https://files.ballistica.net/cache/ba1/d2/20/41cd9666cc82eb905391e5f09358",
"assets/build/ba_data/audio/pixieHit2.ogg": "https://files.ballistica.net/cache/ba1/1b/c8/436ef74c333eab1087f534d51a87",
"assets/build/ba_data/audio/playerDeath.ogg": "https://files.ballistica.net/cache/ba1/87/9a/d7d892c820ea7dd0398cac88ffa1",
"assets/build/ba_data/audio/playerLeft.ogg": "https://files.ballistica.net/cache/ba1/04/b7/16c085238f547631378195827698",
"assets/build/ba_data/audio/pop01.ogg": "https://files.ballistica.net/cache/ba1/98/71/28a573d9a037e568e178359bb02b",
"assets/build/ba_data/audio/powerdown01.ogg": "https://files.ballistica.net/cache/ba1/ed/a7/8e3a7511ab0fafbb9b52445348f4",
"assets/build/ba_data/audio/powerup01.ogg": "https://files.ballistica.net/cache/ba1/3d/f3/d1e6660cd061b5a1b67b0e98088b",
"assets/build/ba_data/audio/punch01.ogg": "https://files.ballistica.net/cache/ba1/f7/0d/22cce48b0346ec0d948b9808dff3",
"assets/build/ba_data/audio/punchStrong01.ogg": "https://files.ballistica.net/cache/ba1/80/4c/00f81efc80a2c68212f62fcc7b01",
"assets/build/ba_data/audio/punchStrong02.ogg": "https://files.ballistica.net/cache/ba1/c3/1d/eaf2396f4e9127b7a66197b4ad75",
"assets/build/ba_data/audio/punchSwish.ogg": "https://files.ballistica.net/cache/ba1/b4/d6/008534042a94cd99523bfcabdcbd",
"assets/build/ba_data/audio/punchWeak01.ogg": "https://files.ballistica.net/cache/ba1/d0/94/53212b0bf039671333388100c9aa",
"assets/build/ba_data/audio/raceBeep1.ogg": "https://files.ballistica.net/cache/ba1/e9/c2/22fd9db278c0a441661b7a14f398",
"assets/build/ba_data/audio/raceBeep2.ogg": "https://files.ballistica.net/cache/ba1/2a/86/6d59a75d7cfaaf2f5d38d55015b5",
"assets/build/ba_data/audio/refWhistle.ogg": "https://files.ballistica.net/cache/ba1/92/d7/d8b7b01eccf13f58aceaee05bf79",
"assets/build/ba_data/audio/robot1.ogg": "https://files.ballistica.net/cache/ba1/25/8a/dd1abdace3a4fc3e73586d8bb103",
"assets/build/ba_data/audio/robot2.ogg": "https://files.ballistica.net/cache/ba1/27/b3/ed6ccb5391bb5b0d4415e09afc37",
"assets/build/ba_data/audio/robot3.ogg": "https://files.ballistica.net/cache/ba1/2b/a9/2b958ed91060cd3e9ab1b657a71f",
"assets/build/ba_data/audio/robot4.ogg": "https://files.ballistica.net/cache/ba1/15/77/ebf9129bc8cf2428418d7484b19d",
"assets/build/ba_data/audio/robotDeath.ogg": "https://files.ballistica.net/cache/ba1/52/1f/578eabdd26e645f88ae7b3715d82",
"assets/build/ba_data/audio/robotFall.ogg": "https://files.ballistica.net/cache/ba1/61/f1/1f4bf39e3d496541f8f117128896",
"assets/build/ba_data/audio/robotHit1.ogg": "https://files.ballistica.net/cache/ba1/7e/e7/c8c8fc38fd68434de77eb22af479",
"assets/build/ba_data/audio/robotHit2.ogg": "https://files.ballistica.net/cache/ba1/a1/e4/3f07c6f8dc90781609ccf73f2bfe",
"assets/build/ba_data/audio/runAwayMusic.ogg": "https://files.ballistica.net/cache/ba1/ba/1c/b684591f05af402c5a77d9e78ef9",
"assets/build/ba_data/audio/santa01.ogg": "https://files.ballistica.net/cache/ba1/87/f7/06c8bd9468c30fc04993ce43c813",
"assets/build/ba_data/audio/santa02.ogg": "https://files.ballistica.net/cache/ba1/b5/86/09edb5a7df6eb3a675ca5821a91c",
"assets/build/ba_data/audio/santa03.ogg": "https://files.ballistica.net/cache/ba1/41/5f/4db6735c4cb1acca84e930fc05b7",
"assets/build/ba_data/audio/santa04.ogg": "https://files.ballistica.net/cache/ba1/f8/2a/7d03c0fa2f3277b9461963486083",
"assets/build/ba_data/audio/santa05.ogg": "https://files.ballistica.net/cache/ba1/4a/8a/f38d78093c9682ebe4f465f904fe",
"assets/build/ba_data/audio/santaDeath.ogg": "https://files.ballistica.net/cache/ba1/20/e6/ca1615ed18b55fa82d3604ee610f",
"assets/build/ba_data/audio/santaFall.ogg": "https://files.ballistica.net/cache/ba1/6c/e0/4fb79b714b95e304ad92bb9e90cc",
"assets/build/ba_data/audio/santaHit01.ogg": "https://files.ballistica.net/cache/ba1/e1/8a/f1ff4f53b6458b667463df023772",
"assets/build/ba_data/audio/santaHit02.ogg": "https://files.ballistica.net/cache/ba1/a2/21/15ec6792a9bfe46ede252733e2f8",
"assets/build/ba_data/audio/santaHit03.ogg": "https://files.ballistica.net/cache/ba1/47/51/54f71289e17b6a086997116ced97",
"assets/build/ba_data/audio/santaHit04.ogg": "https://files.ballistica.net/cache/ba1/e2/71/4a4b49fa9f6bc3094f63395eb11a",
"assets/build/ba_data/audio/scamper01.ogg": "https://files.ballistica.net/cache/ba1/91/f6/00f6677870b55037e0ca6dfa6bac",
"assets/build/ba_data/audio/scaryMusic.ogg": "https://files.ballistica.net/cache/ba1/af/0d/6d5c74255e2c2659ffea0a84bc18",
"assets/build/ba_data/audio/score.ogg": "https://files.ballistica.net/cache/ba1/f0/fe/7f33e7660c204c5d03f48c2025ee",
"assets/build/ba_data/audio/scoreHit01.ogg": "https://files.ballistica.net/cache/ba1/fb/06/8926f64cf5d49ebb0cf964505e51",
"assets/build/ba_data/audio/scoreHit02.ogg": "https://files.ballistica.net/cache/ba1/c5/5f/898fe8045c83d2c96f713ee12f88",
"assets/build/ba_data/audio/scoreIncrease.ogg": "https://files.ballistica.net/cache/ba1/c1/f4/2790931f749d5322e3ac094fbe79",
"assets/build/ba_data/audio/scoresEpicMusic.ogg": "https://files.ballistica.net/cache/ba1/dc/54/816b47036e51d5b1925eb1fe9cd4",
"assets/build/ba_data/audio/shatter.ogg": "https://files.ballistica.net/cache/ba1/d9/f1/18c6e5aa42b03c7933595240dd8c",
"assets/build/ba_data/audio/shieldDown.ogg": "https://files.ballistica.net/cache/ba1/4b/31/e3cc5bc0d8daa7c0282367e7cd4c",
"assets/build/ba_data/audio/shieldHit.ogg": "https://files.ballistica.net/cache/ba1/ab/8d/2092774e4b2b9dbbd512317a00cf",
"assets/build/ba_data/audio/shieldUp.ogg": "https://files.ballistica.net/cache/ba1/14/87/63c5e81a26d1e0ac5480e4998c7c",
"assets/build/ba_data/audio/skid01.ogg": "https://files.ballistica.net/cache/ba1/1d/1a/6f2f183c0d69dbf7df1b0eac35d8",
"assets/build/ba_data/audio/slowEpicMusic.ogg": "https://files.ballistica.net/cache/ba1/e9/13/983b62ba6759d355e7f3f364cb2f",
"assets/build/ba_data/audio/sparkle01.ogg": "https://files.ballistica.net/cache/ba1/4c/87/9e1d47ec1370cb56a4db96801a76",
"assets/build/ba_data/audio/sparkle02.ogg": "https://files.ballistica.net/cache/ba1/69/d8/3596d410738931b296ac542ac65c",
"assets/build/ba_data/audio/sparkle03.ogg": "https://files.ballistica.net/cache/ba1/06/c0/bed26cac2d94cff65e18b9bd6b44",
"assets/build/ba_data/audio/spawn.ogg": "https://files.ballistica.net/cache/ba1/f6/ca/3d7bb3f383a7c91da33df4b2f1f6",
"assets/build/ba_data/audio/spazAttack01.ogg": "https://files.ballistica.net/cache/ba1/d9/73/f459675f255567824a30ca1162a2",
"assets/build/ba_data/audio/spazAttack02.ogg": "https://files.ballistica.net/cache/ba1/f9/06/6c1b6597396aa774b9eb3ea6fd1d",
"assets/build/ba_data/audio/spazAttack03.ogg": "https://files.ballistica.net/cache/ba1/89/d0/ead2cd4a0a604caa3f47d4e79228",
"assets/build/ba_data/audio/spazAttack04.ogg": "https://files.ballistica.net/cache/ba1/e3/74/ada09133a2b21ad6f8755a63cad8",
"assets/build/ba_data/audio/spazDeath01.ogg": "https://files.ballistica.net/cache/ba1/0f/08/739bfc0e87d84a95ef839520cdd7",
"assets/build/ba_data/audio/spazEff.ogg": "https://files.ballistica.net/cache/ba1/3f/a0/c1003a34f7c31292a62cc97d4184",
"assets/build/ba_data/audio/spazFall01.ogg": "https://files.ballistica.net/cache/ba1/11/78/0f80bb08c63bbf075158c45d8aa3",
"assets/build/ba_data/audio/spazImpact01.ogg": "https://files.ballistica.net/cache/ba1/8c/35/68667339c031f5783e7017915ed6",
"assets/build/ba_data/audio/spazImpact02.ogg": "https://files.ballistica.net/cache/ba1/73/e0/9be5bfface94706af3cc2c164f4a",
"assets/build/ba_data/audio/spazImpact03.ogg": "https://files.ballistica.net/cache/ba1/f3/38/3e7ad049b5600a38f7fe4e3550f0",
"assets/build/ba_data/audio/spazImpact04.ogg": "https://files.ballistica.net/cache/ba1/98/ac/4569ac759052dcf171c14a6674e8",
"assets/build/ba_data/audio/spazJump01.ogg": "https://files.ballistica.net/cache/ba1/71/c4/123f2024c16151c89802f07803a9",
"assets/build/ba_data/audio/spazJump02.ogg": "https://files.ballistica.net/cache/ba1/e2/eb/96b4de3dd6df94bb1f39b8a12e4d",
"assets/build/ba_data/audio/spazJump03.ogg": "https://files.ballistica.net/cache/ba1/7e/42/5e2841e5d426ae0dcde40b59003b",
"assets/build/ba_data/audio/spazJump04.ogg": "https://files.ballistica.net/cache/ba1/88/48/c41610cc9a1b42300686301f69f0",
"assets/build/ba_data/audio/spazOw.ogg": "https://files.ballistica.net/cache/ba1/05/1c/f52026ff9cdd7f99c25d048b8418",
"assets/build/ba_data/audio/spazPickup01.ogg": "https://files.ballistica.net/cache/ba1/17/53/7f0e82a5fef9c2ab8ab0a11da06c",
"assets/build/ba_data/audio/spazScream01.ogg": "https://files.ballistica.net/cache/ba1/5b/8a/d754cdd0d89fef49c0ab8fba67e4",
"assets/build/ba_data/audio/splatter.ogg": "https://files.ballistica.net/cache/ba1/35/3b/2986b53483b342bda57a84bd9e25",
"assets/build/ba_data/audio/sportsMusic.ogg": "https://files.ballistica.net/cache/ba1/0a/c7/04ddd16a99f3b71850053c4931ba",
"assets/build/ba_data/audio/stickyImpact.ogg": "https://files.ballistica.net/cache/ba1/7c/97/b784f6e54ced9b9e7084747b4efc",
"assets/build/ba_data/audio/superPunch.ogg": "https://files.ballistica.net/cache/ba1/e7/6a/21e2dd4fe3a12d4f9db10e5647bd",
"assets/build/ba_data/audio/superhero1.ogg": "https://files.ballistica.net/cache/ba1/c1/dd/dee32573a58ffd83cc00689ccfb4",
"assets/build/ba_data/audio/superhero2.ogg": "https://files.ballistica.net/cache/ba1/00/fd/d2d3bd1ad913b7159873b58706f8",
"assets/build/ba_data/audio/superhero3.ogg": "https://files.ballistica.net/cache/ba1/aa/1c/67e2a563354aff2460cbe2eccf52",
"assets/build/ba_data/audio/superhero4.ogg": "https://files.ballistica.net/cache/ba1/81/96/7bb011d80de9182b56f0a4872d0d",
"assets/build/ba_data/audio/superheroDeath.ogg": "https://files.ballistica.net/cache/ba1/85/d0/f061401183a4f6f5fc91995b6f76",
"assets/build/ba_data/audio/superheroFall.ogg": "https://files.ballistica.net/cache/ba1/cc/f5/a2cf691726deb992081aecafb1bc",
"assets/build/ba_data/audio/superheroHit1.ogg": "https://files.ballistica.net/cache/ba1/d4/e9/8d6126156d834c8ae84afdb28c33",
"assets/build/ba_data/audio/superheroHit2.ogg": "https://files.ballistica.net/cache/ba1/46/68/dedbd03193754b3f937324f5d3c4",
"assets/build/ba_data/audio/survivalMusic.ogg": "https://files.ballistica.net/cache/ba1/47/fc/601d034cf62aba583cac5521b627",
"assets/build/ba_data/audio/swip.ogg": "https://files.ballistica.net/cache/ba1/7b/8a/998711379e489600665c5116d2b6",
"assets/build/ba_data/audio/swip2.ogg": "https://files.ballistica.net/cache/ba1/2a/fc/e47bedc09e64e4d5c51ddaa2b18c",
"assets/build/ba_data/audio/swish.ogg": "https://files.ballistica.net/cache/ba1/6e/6c/6ffd6f7c142bd7cf05c98df1cd5a",
"assets/build/ba_data/audio/swish2.ogg": "https://files.ballistica.net/cache/ba1/a9/6c/bb02b87eeafb72b88c2b17c7f443",
"assets/build/ba_data/audio/swish3.ogg": "https://files.ballistica.net/cache/ba1/a2/72/fdde40e2eedde9647467a272e39f",
"assets/build/ba_data/audio/tap.ogg": "https://files.ballistica.net/cache/ba1/3b/3f/b5dd305b652fbd817a5c8a9c09d1",
"assets/build/ba_data/audio/technoHit01.ogg": "https://files.ballistica.net/cache/ba1/65/20/06100b2ec255563927c0185c3199",
"assets/build/ba_data/audio/tick.ogg": "https://files.ballistica.net/cache/ba1/97/57/dbfc82a25ba4153be0a85e420d3f",
"assets/build/ba_data/audio/ticking.ogg": "https://files.ballistica.net/cache/ba1/f0/fc/9697c52a8298000f63b8d0d9fc3c",
"assets/build/ba_data/audio/tickingCrazy.ogg": "https://files.ballistica.net/cache/ba1/13/be/cb51602790cee789598ee4ba237c",
"assets/build/ba_data/audio/toTheDeathMusic.ogg": "https://files.ballistica.net/cache/ba1/ad/f3/f1219e2e320c1bca00dc7be034fd",
"assets/build/ba_data/audio/trashRummage.ogg": "https://files.ballistica.net/cache/ba1/b6/40/b42ee407cb6460ab45bcfd0eb707",
"assets/build/ba_data/audio/victoryMusic.ogg": "https://files.ballistica.net/cache/ba1/45/69/89e1f27a004507a98a63a72f8632",
"assets/build/ba_data/audio/warnBeep.ogg": "https://files.ballistica.net/cache/ba1/6e/e1/d2384fdc2987710aa24a8cb8248d",
"assets/build/ba_data/audio/warnBeeps.ogg": "https://files.ballistica.net/cache/ba1/48/ac/c12870dbac3d12e705f9354aec72",
"assets/build/ba_data/audio/warrior1.ogg": "https://files.ballistica.net/cache/ba1/42/d7/6fde00abcddcc7469436820f9155",
"assets/build/ba_data/audio/warrior2.ogg": "https://files.ballistica.net/cache/ba1/66/13/daec33822de45d32c50dda7a9db0",
"assets/build/ba_data/audio/warrior3.ogg": "https://files.ballistica.net/cache/ba1/59/8d/d97ea49d6f0422da11823e18c3df",
"assets/build/ba_data/audio/warrior4.ogg": "https://files.ballistica.net/cache/ba1/ac/dc/7f7682b4a5603ef27650b2456ac7",
"assets/build/ba_data/audio/warriorDeath.ogg": "https://files.ballistica.net/cache/ba1/88/71/76555d9b53b029d9d973c311c450",
"assets/build/ba_data/audio/warriorFall.ogg": "https://files.ballistica.net/cache/ba1/78/93/654e954bf52df964ba69ffe5dc5a",
"assets/build/ba_data/audio/warriorHit1.ogg": "https://files.ballistica.net/cache/ba1/b9/00/1dea8f4393e71d373051f7d6e83b",
"assets/build/ba_data/audio/warriorHit2.ogg": "https://files.ballistica.net/cache/ba1/b7/6c/dd05e76055fc57f9d19fd232f3e2",
"assets/build/ba_data/audio/whenJohnnyComesMarchingHomeMusic.ogg": "https://files.ballistica.net/cache/ba1/80/18/2e9f72069cebbc4fd4f66009f686",
"assets/build/ba_data/audio/witch1.ogg": "https://files.ballistica.net/cache/ba1/d9/29/d859837ebe122cb682973be4f8f8",
"assets/build/ba_data/audio/witch2.ogg": "https://files.ballistica.net/cache/ba1/90/e2/f1c1dde870e74de9953991ee3ae9",
"assets/build/ba_data/audio/witch3.ogg": "https://files.ballistica.net/cache/ba1/bd/22/85eef9e2d2495518d0736110a95b",
"assets/build/ba_data/audio/witch4.ogg": "https://files.ballistica.net/cache/ba1/54/ae/a2428af20c6085636f85a0f2a140",
"assets/build/ba_data/audio/witchDeath.ogg": "https://files.ballistica.net/cache/ba1/09/a4/6ba2c6ff8ca85d7016962d0947fa",
"assets/build/ba_data/audio/witchFall.ogg": "https://files.ballistica.net/cache/ba1/94/9d/8497f02c50a0756a87a2f4b78fa5",
"assets/build/ba_data/audio/witchHit1.ogg": "https://files.ballistica.net/cache/ba1/fc/47/d650d4a82529f11502861723f13e",
"assets/build/ba_data/audio/witchHit2.ogg": "https://files.ballistica.net/cache/ba1/50/45/b8809360598df3f0203f62e788b0",
"assets/build/ba_data/audio/wizard1.ogg": "https://files.ballistica.net/cache/ba1/9c/38/0c6a42cd90025cad61c43c7abc8a",
"assets/build/ba_data/audio/wizard2.ogg": "https://files.ballistica.net/cache/ba1/12/56/4cb851ee7ebc308ae1a50771cf8f",
"assets/build/ba_data/audio/wizard3.ogg": "https://files.ballistica.net/cache/ba1/e6/8f/5094937a979dbcc509713d2f4366",
"assets/build/ba_data/audio/wizard4.ogg": "https://files.ballistica.net/cache/ba1/ed/60/d29dc09f12486e9ac0abd95b1270",
"assets/build/ba_data/audio/wizardDeath.ogg": "https://files.ballistica.net/cache/ba1/45/c3/f9c6f78915a06787cbbb3750521c",
"assets/build/ba_data/audio/wizardFall.ogg": "https://files.ballistica.net/cache/ba1/7d/8d/a6769c6a8f66d17a1417de4ed09f",
"assets/build/ba_data/audio/wizardHit1.ogg": "https://files.ballistica.net/cache/ba1/9f/c2/5025c9fe242b84e3ada693fa835f",
"assets/build/ba_data/audio/wizardHit2.ogg": "https://files.ballistica.net/cache/ba1/b9/04/c83d85abe1aed831c025e664bb80",
"assets/build/ba_data/audio/woodDebrisFall.ogg": "https://files.ballistica.net/cache/ba1/90/f0/9c1b3cc64bc841f128075dc3d4b1",
"assets/build/ba_data/audio/wrestler1.ogg": "https://files.ballistica.net/cache/ba1/bb/d1/907e03fae0606f3d26e1b8872e4c",
"assets/build/ba_data/audio/wrestler2.ogg": "https://files.ballistica.net/cache/ba1/c8/2d/62f5f4cea7ad690a9a675d27eb42",
"assets/build/ba_data/audio/wrestler3.ogg": "https://files.ballistica.net/cache/ba1/f8/15/686b9d131de8e70841bf449bbf76",
"assets/build/ba_data/audio/wrestler4.ogg": "https://files.ballistica.net/cache/ba1/e2/2b/0e029172c72f1164813ec3f883be",
"assets/build/ba_data/audio/wrestlerDeath.ogg": "https://files.ballistica.net/cache/ba1/ec/ea/2c38a9feadd86c2468a0e26a3871",
"assets/build/ba_data/audio/wrestlerFall.ogg": "https://files.ballistica.net/cache/ba1/f8/68/a86da4d0fa832d29d558ec3bf925",
"assets/build/ba_data/audio/wrestlerHit1.ogg": "https://files.ballistica.net/cache/ba1/40/7e/9ed1d178e0b312277caaba2971d5",
"assets/build/ba_data/audio/wrestlerHit2.ogg": "https://files.ballistica.net/cache/ba1/e0/66/f8b3b030d5d4d1c54d9b40959665",
"assets/build/ba_data/audio/zoeAttack01.ogg": "https://files.ballistica.net/cache/ba1/30/ee/c1222e9cbed55980b40127ba14ff",
"assets/build/ba_data/audio/zoeAttack02.ogg": "https://files.ballistica.net/cache/ba1/dc/fd/d6ecaa6e91b94c1a0b604bd437f4",
"assets/build/ba_data/audio/zoeAttack03.ogg": "https://files.ballistica.net/cache/ba1/5d/9c/27defeed77fbf787151b89955d2f",
"assets/build/ba_data/audio/zoeAttack04.ogg": "https://files.ballistica.net/cache/ba1/f8/0c/acd7cde5c8e1e01d92c63b70a644",
"assets/build/ba_data/audio/zoeDeath01.ogg": "https://files.ballistica.net/cache/ba1/ff/90/ebfcd5852f95e6f3d2f8b6c8c9ce",
"assets/build/ba_data/audio/zoeEff.ogg": "https://files.ballistica.net/cache/ba1/ee/0c/bfd0f105580f41ec43c14e1a4d26",
"assets/build/ba_data/audio/zoeFall01.ogg": "https://files.ballistica.net/cache/ba1/cf/7c/9b8e9f1d0387c7fed639511c882b",
"assets/build/ba_data/audio/zoeImpact01.ogg": "https://files.ballistica.net/cache/ba1/17/3d/7a8c78e0aa6db9aabff2851fa7f7",
"assets/build/ba_data/audio/zoeImpact02.ogg": "https://files.ballistica.net/cache/ba1/c1/bd/b54f9081b11bd5ec04cf4a56013a",
"assets/build/ba_data/audio/zoeImpact03.ogg": "https://files.ballistica.net/cache/ba1/02/02/008abd88c1deeac85ed88d25b42d",
"assets/build/ba_data/audio/zoeImpact04.ogg": "https://files.ballistica.net/cache/ba1/6f/b2/a1a774e466433f0c5326ffdfcb61",
"assets/build/ba_data/audio/zoeJump01.ogg": "https://files.ballistica.net/cache/ba1/77/9c/e0529c9dd0a732b4b195676e14f2",
"assets/build/ba_data/audio/zoeJump02.ogg": "https://files.ballistica.net/cache/ba1/88/d8/88ec7aa0c8c1b5219658f78ac2d3",
"assets/build/ba_data/audio/zoeJump03.ogg": "https://files.ballistica.net/cache/ba1/8c/2a/caa5b929b32aa5226d96df5733a9",
"assets/build/ba_data/audio/zoeOw.ogg": "https://files.ballistica.net/cache/ba1/51/eb/0a567253cc08c94c5d315a64d9af",
"assets/build/ba_data/audio/zoePickup01.ogg": "https://files.ballistica.net/cache/ba1/bc/8f/a9c51a09c418136e386b7fdf21c7",
"assets/build/ba_data/audio/zoeScream01.ogg": "https://files.ballistica.net/cache/ba1/02/e5/84916e123f47ccf11ddda380d699",
"assets/build/ba_data/audio/achievement.ogg": "https://files.ballistica.net/cache/ba1/cb/ab/2d6ea35efc25d60a438e7ece17da",
"assets/build/ba_data/audio/actionHero1.ogg": "https://files.ballistica.net/cache/ba1/5f/f8/e28c70dae7478253a593055b7f39",
"assets/build/ba_data/audio/actionHero2.ogg": "https://files.ballistica.net/cache/ba1/a2/9c/1e5d37cc7f17faa51b8687bdfbdd",
"assets/build/ba_data/audio/actionHero3.ogg": "https://files.ballistica.net/cache/ba1/83/c6/af98a7f49372ee42ae4e13b94c1f",
"assets/build/ba_data/audio/actionHero4.ogg": "https://files.ballistica.net/cache/ba1/a1/ec/92187fe07c7104ecb95d592745ce",
"assets/build/ba_data/audio/actionHeroDeath.ogg": "https://files.ballistica.net/cache/ba1/b5/40/f13df4e9759bf6a07d16126ad209",
"assets/build/ba_data/audio/actionHeroFall.ogg": "https://files.ballistica.net/cache/ba1/f6/e2/e79abf259be67f747227f703897b",
"assets/build/ba_data/audio/actionHeroHit1.ogg": "https://files.ballistica.net/cache/ba1/5e/86/aad0e972ecd05cbb47c0d763ed94",
"assets/build/ba_data/audio/actionHeroHit2.ogg": "https://files.ballistica.net/cache/ba1/b3/3b/061719631d7b6456007fee1ea7cb",
"assets/build/ba_data/audio/activateBeep.ogg": "https://files.ballistica.net/cache/ba1/78/5e/925c8c553299727135831a57b70e",
"assets/build/ba_data/audio/agent1.ogg": "https://files.ballistica.net/cache/ba1/19/73/5845fb4cb3452a4e7cc406d6c308",
"assets/build/ba_data/audio/agent2.ogg": "https://files.ballistica.net/cache/ba1/32/7f/ca537f1df01d03a883edd3e56465",
"assets/build/ba_data/audio/agent3.ogg": "https://files.ballistica.net/cache/ba1/bb/8a/543d0f44b6c7e7435f309db67830",
"assets/build/ba_data/audio/agent4.ogg": "https://files.ballistica.net/cache/ba1/64/f7/968ec161fbd4664dcb02adad36bc",
"assets/build/ba_data/audio/agentDeath.ogg": "https://files.ballistica.net/cache/ba1/cc/eb/0f4fb1f08488b983696f9bae1f04",
"assets/build/ba_data/audio/agentFall.ogg": "https://files.ballistica.net/cache/ba1/2b/07/8147ad5e3ed939b20917bca07ce7",
"assets/build/ba_data/audio/agentHit1.ogg": "https://files.ballistica.net/cache/ba1/28/97/81c2486a7f2a21b01c0c37897851",
"assets/build/ba_data/audio/agentHit2.ogg": "https://files.ballistica.net/cache/ba1/97/5b/68d5e47bcc38b41c1c021fca6734",
"assets/build/ba_data/audio/alarm.ogg": "https://files.ballistica.net/cache/ba1/e6/c7/494a9ab00fcbefcafc5708b965fc",
"assets/build/ba_data/audio/ali1.ogg": "https://files.ballistica.net/cache/ba1/ab/06/a5101ba8bf3ece607dcab63f638c",
"assets/build/ba_data/audio/ali2.ogg": "https://files.ballistica.net/cache/ba1/eb/30/68ed4470cd99fd9d5e92da402e79",
"assets/build/ba_data/audio/ali3.ogg": "https://files.ballistica.net/cache/ba1/08/45/fd48e69b43ad0c407a3c6538187d",
"assets/build/ba_data/audio/ali4.ogg": "https://files.ballistica.net/cache/ba1/64/82/010bfa0a8c42b16590fd3567a5d3",
"assets/build/ba_data/audio/aliDeath.ogg": "https://files.ballistica.net/cache/ba1/61/45/bc437757318afdaafcd939295257",
"assets/build/ba_data/audio/aliFall.ogg": "https://files.ballistica.net/cache/ba1/f1/35/4e5c372963c6151bf8c4e330add0",
"assets/build/ba_data/audio/aliHit1.ogg": "https://files.ballistica.net/cache/ba1/a1/bf/236d797fefd739a0faca9d43a05e",
"assets/build/ba_data/audio/aliHit2.ogg": "https://files.ballistica.net/cache/ba1/3e/63/dcb20edee20a60d5a1ed442aaeb9",
"assets/build/ba_data/audio/alien1.ogg": "https://files.ballistica.net/cache/ba1/92/1d/ecbba63b9a4f09362fdae312bbed",
"assets/build/ba_data/audio/alien2.ogg": "https://files.ballistica.net/cache/ba1/d6/47/894134eeb6d969d2ccd62325d54f",
"assets/build/ba_data/audio/alien3.ogg": "https://files.ballistica.net/cache/ba1/6d/14/2f5afc9cab01c13d4101014497cb",
"assets/build/ba_data/audio/alien4.ogg": "https://files.ballistica.net/cache/ba1/31/9e/cf4924e83b1295ac46e3ef6b3200",
"assets/build/ba_data/audio/alienDeath.ogg": "https://files.ballistica.net/cache/ba1/5c/06/88114cbc4523a6967fb068a31d72",
"assets/build/ba_data/audio/alienFall.ogg": "https://files.ballistica.net/cache/ba1/50/e6/19e2c76c858a0062e23630d2a739",
"assets/build/ba_data/audio/alienHit1.ogg": "https://files.ballistica.net/cache/ba1/67/62/a4501475b13c36e3cd20c08535a1",
"assets/build/ba_data/audio/alienHit2.ogg": "https://files.ballistica.net/cache/ba1/d5/c3/c4616a4d3a68a972bd53944839bb",
"assets/build/ba_data/audio/announceEight.ogg": "https://files.ballistica.net/cache/ba1/42/b1/e686feb97d65eaae220f8fcadfef",
"assets/build/ba_data/audio/announceFive.ogg": "https://files.ballistica.net/cache/ba1/61/a4/7436a7c49e99630ff02d342399c3",
"assets/build/ba_data/audio/announceFour.ogg": "https://files.ballistica.net/cache/ba1/3e/48/20584897c0b9a834dec42da0abbd",
"assets/build/ba_data/audio/announceNine.ogg": "https://files.ballistica.net/cache/ba1/53/e0/6e3980f8662d57259735a1ba74a9",
"assets/build/ba_data/audio/announceOne.ogg": "https://files.ballistica.net/cache/ba1/bb/d8/23d80294d743605ffe300bc13c67",
"assets/build/ba_data/audio/announceSeven.ogg": "https://files.ballistica.net/cache/ba1/c0/70/32a7f0985cb4da92ab54a353434f",
"assets/build/ba_data/audio/announceSix.ogg": "https://files.ballistica.net/cache/ba1/81/8d/a4adf86b22c56301e775eb5ec7c8",
"assets/build/ba_data/audio/announceTen.ogg": "https://files.ballistica.net/cache/ba1/e1/fa/14a0e93ab607c0d0cb59e44d2595",
"assets/build/ba_data/audio/announceThree.ogg": "https://files.ballistica.net/cache/ba1/82/c4/9a3f54546bb67069d4eb28e14bb7",
"assets/build/ba_data/audio/announceTwo.ogg": "https://files.ballistica.net/cache/ba1/75/41/888c7b22351424e71527792c9e2f",
"assets/build/ba_data/audio/assassin1.ogg": "https://files.ballistica.net/cache/ba1/65/d9/1f1d586e426935f4e42d613bf229",
"assets/build/ba_data/audio/assassin2.ogg": "https://files.ballistica.net/cache/ba1/c2/1a/46720c6613b6aa32ece147707383",
"assets/build/ba_data/audio/assassin3.ogg": "https://files.ballistica.net/cache/ba1/e9/4a/42a2dc60b7b889b8ac446fa2bca5",
"assets/build/ba_data/audio/assassin4.ogg": "https://files.ballistica.net/cache/ba1/56/5c/fc77452f83d8f37b2dac7d1b429c",
"assets/build/ba_data/audio/assassinDeath.ogg": "https://files.ballistica.net/cache/ba1/bd/ee/e9e36b97abf7b59e20bacc1cfb06",
"assets/build/ba_data/audio/assassinFall.ogg": "https://files.ballistica.net/cache/ba1/18/a6/613c0cdc89266d007e30942cf933",
"assets/build/ba_data/audio/assassinHit1.ogg": "https://files.ballistica.net/cache/ba1/ed/c5/288fd1d3b1f046e54fe15bfbb0cb",
"assets/build/ba_data/audio/assassinHit2.ogg": "https://files.ballistica.net/cache/ba1/9e/3a/6599fd2c5b164e323f0c1520cd54",
"assets/build/ba_data/audio/bear1.ogg": "https://files.ballistica.net/cache/ba1/4f/56/abd87a1509aae2fe2a916a838738",
"assets/build/ba_data/audio/bear2.ogg": "https://files.ballistica.net/cache/ba1/5a/79/e3d5eb66dad4419a444ef9e78800",
"assets/build/ba_data/audio/bear3.ogg": "https://files.ballistica.net/cache/ba1/40/41/fc30bda6d0512c65f0f5c2af5fd6",
"assets/build/ba_data/audio/bear4.ogg": "https://files.ballistica.net/cache/ba1/11/d0/2098ad6707eb3990ebfa247ed2aa",
"assets/build/ba_data/audio/bearDeath.ogg": "https://files.ballistica.net/cache/ba1/43/7d/f55df97e3ece6d0a35131a7cbd8e",
"assets/build/ba_data/audio/bearFall.ogg": "https://files.ballistica.net/cache/ba1/b8/11/a8fc8e12c61284355395b66f1208",
"assets/build/ba_data/audio/bearHit1.ogg": "https://files.ballistica.net/cache/ba1/d5/5c/401deb576e2b33abecce9f11a1c3",
"assets/build/ba_data/audio/bearHit2.ogg": "https://files.ballistica.net/cache/ba1/f5/c3/f9045d531d4f6a94345f23eef7c2",
"assets/build/ba_data/audio/bellHigh.ogg": "https://files.ballistica.net/cache/ba1/83/12/2563f760c5708d07131b62a3205c",
"assets/build/ba_data/audio/bellLow.ogg": "https://files.ballistica.net/cache/ba1/0c/e7/0b7f120498c93f4b73d9605a9c05",
"assets/build/ba_data/audio/bellMed.ogg": "https://files.ballistica.net/cache/ba1/3c/86/0ffdef9573bed45f5c02b16a0a9c",
"assets/build/ba_data/audio/bigImpact.ogg": "https://files.ballistica.net/cache/ba1/a9/33/6d6856fce790ea58725968ee994f",
"assets/build/ba_data/audio/bigImpact2.ogg": "https://files.ballistica.net/cache/ba1/79/78/c5ae3896392876024d0dc5b09abe",
"assets/build/ba_data/audio/blank.ogg": "https://files.ballistica.net/cache/ba1/36/34/1091be93fc4de625b349619e5d4a",
"assets/build/ba_data/audio/blip.ogg": "https://files.ballistica.net/cache/ba1/56/6a/0a9ce1a16ff567346b5274d4d3fc",
"assets/build/ba_data/audio/block.ogg": "https://files.ballistica.net/cache/ba1/94/8b/612f7219dda2ab10bc915826bc1d",
"assets/build/ba_data/audio/bombDrop01.ogg": "https://files.ballistica.net/cache/ba1/48/e4/7059abf02828e2b2028ece33d4fb",
"assets/build/ba_data/audio/bombDrop02.ogg": "https://files.ballistica.net/cache/ba1/c4/09/667eaeab8e953e266b2590174392",
"assets/build/ba_data/audio/bombRoll01.ogg": "https://files.ballistica.net/cache/ba1/eb/e6/449ff777ac20f3dde1dbc8fcbae5",
"assets/build/ba_data/audio/bones1.ogg": "https://files.ballistica.net/cache/ba1/86/9e/cdc4cc56d18de403c997721c296e",
"assets/build/ba_data/audio/bones2.ogg": "https://files.ballistica.net/cache/ba1/a4/27/84d4389613cb70e6b6b01c518342",
"assets/build/ba_data/audio/bones3.ogg": "https://files.ballistica.net/cache/ba1/b5/75/8fc4027848a6b070d8a5d738c46f",
"assets/build/ba_data/audio/bonesDeath.ogg": "https://files.ballistica.net/cache/ba1/6b/b6/0297afbded9af2527b42229a8594",
"assets/build/ba_data/audio/bonesFall.ogg": "https://files.ballistica.net/cache/ba1/fc/6c/83feba62ebdefa5c54fab3e1ff7f",
"assets/build/ba_data/audio/boo.ogg": "https://files.ballistica.net/cache/ba1/ec/db/582d0730aa22e1f96329e7b5bd22",
"assets/build/ba_data/audio/boxDrop.ogg": "https://files.ballistica.net/cache/ba1/ca/32/f71e4e7c6c821fe3cedf729ceda6",
"assets/build/ba_data/audio/boxingBell.ogg": "https://files.ballistica.net/cache/ba1/d9/f6/a76ab7158580e9900cf8b09dc5c7",
"assets/build/ba_data/audio/bunny1.ogg": "https://files.ballistica.net/cache/ba1/50/3d/ded806bbb47d207d85487989ef75",
"assets/build/ba_data/audio/bunny2.ogg": "https://files.ballistica.net/cache/ba1/6c/0c/1a7063168bb67d625f095409ba22",
"assets/build/ba_data/audio/bunny3.ogg": "https://files.ballistica.net/cache/ba1/77/40/5c29e993c896e5aac26173c297bc",
"assets/build/ba_data/audio/bunny4.ogg": "https://files.ballistica.net/cache/ba1/b9/8e/2c3a9bc55c140bea8b22bfc74e05",
"assets/build/ba_data/audio/bunnyDeath.ogg": "https://files.ballistica.net/cache/ba1/c8/b6/0fae511916818a5d2e96f997bd53",
"assets/build/ba_data/audio/bunnyFall.ogg": "https://files.ballistica.net/cache/ba1/16/42/f56c2241fc379d4fed2e859de91d",
"assets/build/ba_data/audio/bunnyHit1.ogg": "https://files.ballistica.net/cache/ba1/ed/19/4c7adb273ecd9a4daeb0aa1871e7",
"assets/build/ba_data/audio/bunnyHit2.ogg": "https://files.ballistica.net/cache/ba1/56/46/6089ad88b17cc7b54cb8278e0827",
"assets/build/ba_data/audio/bunnyJump.ogg": "https://files.ballistica.net/cache/ba1/51/89/51bf0ef263ce6e93fe4952053ee2",
"assets/build/ba_data/audio/cashRegister.ogg": "https://files.ballistica.net/cache/ba1/19/ba/474d5016013849089bd53fea405b",
"assets/build/ba_data/audio/cashRegister2.ogg": "https://files.ballistica.net/cache/ba1/57/2a/9f46332bf8fec2f9edeb1e2b41d0",
"assets/build/ba_data/audio/charSelectMusic.ogg": "https://files.ballistica.net/cache/ba1/88/43/ca42428df5ddd98afcdc230d3bfa",
"assets/build/ba_data/audio/cheer.ogg": "https://files.ballistica.net/cache/ba1/88/d3/7c4a6ce953e32bea70f2a960aa2d",
"assets/build/ba_data/audio/click01.ogg": "https://files.ballistica.net/cache/ba1/46/52/58ade1fd15a5b105156d94435dc7",
"assets/build/ba_data/audio/corkPop.ogg": "https://files.ballistica.net/cache/ba1/bb/64/00f03c3c8dc1a4de948135d0313b",
"assets/build/ba_data/audio/cowboy1.ogg": "https://files.ballistica.net/cache/ba1/68/13/107eb7b0ab45fe2c97c20d0aa0d1",
"assets/build/ba_data/audio/cowboy2.ogg": "https://files.ballistica.net/cache/ba1/4e/20/d14b17a7c7dfd6c1e7a581ab7c13",
"assets/build/ba_data/audio/cowboy3.ogg": "https://files.ballistica.net/cache/ba1/b3/13/24d779bccb5d629232bad9561696",
"assets/build/ba_data/audio/cowboy4.ogg": "https://files.ballistica.net/cache/ba1/45/4f/e74a3a2af2266f6cb84634794b43",
"assets/build/ba_data/audio/cowboyDeath.ogg": "https://files.ballistica.net/cache/ba1/32/60/f8f038fd6051536073aeb3afc8f6",
"assets/build/ba_data/audio/cowboyFall.ogg": "https://files.ballistica.net/cache/ba1/1a/7b/40760c8cf56f49cfd5f50ebeb1b8",
"assets/build/ba_data/audio/cowboyHit1.ogg": "https://files.ballistica.net/cache/ba1/a3/11/ed69e9bf10790291bdc4ff58c4f3",
"assets/build/ba_data/audio/cowboyHit2.ogg": "https://files.ballistica.net/cache/ba1/c6/50/1fd260288b18d4713de3953ff80b",
"assets/build/ba_data/audio/crowdChant.ogg": "https://files.ballistica.net/cache/ba1/0a/84/2866198cedd9f6d6130cb1c515b4",
"assets/build/ba_data/audio/cyborg1.ogg": "https://files.ballistica.net/cache/ba1/e3/9f/471f6a84f196ed866b2174bf9c92",
"assets/build/ba_data/audio/cyborg2.ogg": "https://files.ballistica.net/cache/ba1/43/58/d8eaf3477f352bf2639f6bc69338",
"assets/build/ba_data/audio/cyborg3.ogg": "https://files.ballistica.net/cache/ba1/91/71/6eca76defc42c58c0e4d3a563cce",
"assets/build/ba_data/audio/cyborg4.ogg": "https://files.ballistica.net/cache/ba1/34/62/4ff3eafad0116710a8b43167899d",
"assets/build/ba_data/audio/cyborgDeath.ogg": "https://files.ballistica.net/cache/ba1/39/15/2b868c62b7e57871f86456702e2d",
"assets/build/ba_data/audio/cyborgFall.ogg": "https://files.ballistica.net/cache/ba1/5e/03/a97150ab79c3c5f6949f257f0d05",
"assets/build/ba_data/audio/cyborgHit1.ogg": "https://files.ballistica.net/cache/ba1/e9/2a/c2ceda9d5108ec9da01f6eaf6d66",
"assets/build/ba_data/audio/cyborgHit2.ogg": "https://files.ballistica.net/cache/ba1/f2/4a/57ca0f7f061c81894ae31240fb68",
"assets/build/ba_data/audio/cymbal.ogg": "https://files.ballistica.net/cache/ba1/65/3d/096ba6468a191c2bb0841715cf0e",
"assets/build/ba_data/audio/debrisFall.ogg": "https://files.ballistica.net/cache/ba1/c5/2c/daeea5fe8ee17bb98fccd964294d",
"assets/build/ba_data/audio/deek.ogg": "https://files.ballistica.net/cache/ba1/30/8c/7443bed085ff0142cf02a1212bd5",
"assets/build/ba_data/audio/deek2.ogg": "https://files.ballistica.net/cache/ba1/02/9b/d82898d809fcdc31536434c4664f",
"assets/build/ba_data/audio/ding.ogg": "https://files.ballistica.net/cache/ba1/a6/05/8d63d138a3def941a5fa3e9d29d8",
"assets/build/ba_data/audio/dingSmall.ogg": "https://files.ballistica.net/cache/ba1/2e/99/c2bbeb7ad34af7cdc2e5bdc0162e",
"assets/build/ba_data/audio/dingSmallHigh.ogg": "https://files.ballistica.net/cache/ba1/56/34/44a17eb4a03b0af4cda2635e6f9f",
"assets/build/ba_data/audio/dripity.ogg": "https://files.ballistica.net/cache/ba1/25/2c/a0c8f8b402266692fc619caecee9",
"assets/build/ba_data/audio/drumRoll.ogg": "https://files.ballistica.net/cache/ba1/77/1e/35b3f6e0536006c7cb49f0cb10df",
"assets/build/ba_data/audio/error.ogg": "https://files.ballistica.net/cache/ba1/66/94/51b1f7f236d527e5c6b7aaa38df4",
"assets/build/ba_data/audio/explosion01.ogg": "https://files.ballistica.net/cache/ba1/28/8b/354d177513c116e8ffe8c4a33170",
"assets/build/ba_data/audio/explosion02.ogg": "https://files.ballistica.net/cache/ba1/a2/85/7e036bfe7ca05f71d32da98c5f97",
"assets/build/ba_data/audio/explosion03.ogg": "https://files.ballistica.net/cache/ba1/a8/79/bece5fe5fd9f4e7d51efa1dc1456",
"assets/build/ba_data/audio/explosion04.ogg": "https://files.ballistica.net/cache/ba1/cb/21/360b3d69ad2f26ccba5357e39e1a",
"assets/build/ba_data/audio/explosion05.ogg": "https://files.ballistica.net/cache/ba1/ca/48/22180176ef8ca975c15dc0fe8fdf",
"assets/build/ba_data/audio/fanfare.ogg": "https://files.ballistica.net/cache/ba1/e9/08/ca9f982446c394120e8b1087c839",
"assets/build/ba_data/audio/flagCatcherMusic.ogg": "https://files.ballistica.net/cache/ba1/f8/4a/e6e9f5e1748fc0f5da61b4270051",
"assets/build/ba_data/audio/flyingMusic.ogg": "https://files.ballistica.net/cache/ba1/e0/50/ac8414bb072b1003689d2e25aa55",
"assets/build/ba_data/audio/foghorn.ogg": "https://files.ballistica.net/cache/ba1/ad/81/f239c532e098a75ec31b0ea1be18",
"assets/build/ba_data/audio/footImpact01.ogg": "https://files.ballistica.net/cache/ba1/95/cd/b1cdf4f368f181485364c4523e1f",
"assets/build/ba_data/audio/footImpact02.ogg": "https://files.ballistica.net/cache/ba1/cb/62/1aa42e0ffd1fc6a2d903d41c2ffb",
"assets/build/ba_data/audio/footImpact03.ogg": "https://files.ballistica.net/cache/ba1/1d/1e/51ac91bcb961f6919c239de832db",
"assets/build/ba_data/audio/forwardMarchMusic.ogg": "https://files.ballistica.net/cache/ba1/ae/e2/f126bdf270f6e0e0df8960d47e62",
"assets/build/ba_data/audio/freeze.ogg": "https://files.ballistica.net/cache/ba1/aa/09/04cfd6bd13c85d4fea0c1ea64cd2",
"assets/build/ba_data/audio/frosty01.ogg": "https://files.ballistica.net/cache/ba1/fe/2c/523d70333081c5a601754af177b8",
"assets/build/ba_data/audio/frosty02.ogg": "https://files.ballistica.net/cache/ba1/7e/31/ef9b158f59f7c2ba999d6e7ab659",
"assets/build/ba_data/audio/frosty03.ogg": "https://files.ballistica.net/cache/ba1/85/b0/c0af5d2822fb8b16d54547de91e0",
"assets/build/ba_data/audio/frosty04.ogg": "https://files.ballistica.net/cache/ba1/a5/30/8df73c185bfdea6ff68ade0cc1fd",
"assets/build/ba_data/audio/frosty05.ogg": "https://files.ballistica.net/cache/ba1/3d/51/8cdde58436dbf07811e17960530c",
"assets/build/ba_data/audio/frostyDeath.ogg": "https://files.ballistica.net/cache/ba1/81/4f/94d1d3a0a0d4d76ec157c9e62f5d",
"assets/build/ba_data/audio/frostyFall.ogg": "https://files.ballistica.net/cache/ba1/66/f1/189455d59d83befc748023eb5a3b",
"assets/build/ba_data/audio/frostyHit01.ogg": "https://files.ballistica.net/cache/ba1/93/b9/efb726844982629e84436eff6b70",
"assets/build/ba_data/audio/frostyHit02.ogg": "https://files.ballistica.net/cache/ba1/5a/c6/f5468203e6a1e7a88cd1df36cddb",
"assets/build/ba_data/audio/frostyHit03.ogg": "https://files.ballistica.net/cache/ba1/11/a7/10058c88ae6c0616c242a81225cb",
"assets/build/ba_data/audio/fuse01.ogg": "https://files.ballistica.net/cache/ba1/c0/6d/cea041c63d5e6c2673a155ef3b5e",
"assets/build/ba_data/audio/gladiator1.ogg": "https://files.ballistica.net/cache/ba1/83/c3/7f27a72348692a28f3d934721a70",
"assets/build/ba_data/audio/gladiator2.ogg": "https://files.ballistica.net/cache/ba1/c6/25/9f7e88466aa35312b576671efcb9",
"assets/build/ba_data/audio/gladiator3.ogg": "https://files.ballistica.net/cache/ba1/9a/a0/dd97f411483f32e9ca28e97978ec",
"assets/build/ba_data/audio/gladiator4.ogg": "https://files.ballistica.net/cache/ba1/e9/0b/1037b4fd77a0609a731421142069",
"assets/build/ba_data/audio/gladiatorDeath.ogg": "https://files.ballistica.net/cache/ba1/58/dc/242c3c9db9ea287a7ca84d4b58f2",
"assets/build/ba_data/audio/gladiatorFall.ogg": "https://files.ballistica.net/cache/ba1/9e/b4/e97cb15384b43bc44848a9940fcd",
"assets/build/ba_data/audio/gladiatorHit1.ogg": "https://files.ballistica.net/cache/ba1/78/1c/2c14f09c156faaf53bc97f1fa4ab",
"assets/build/ba_data/audio/gladiatorHit2.ogg": "https://files.ballistica.net/cache/ba1/ee/0b/724da82d345acce6d24f1f78aebe",
"assets/build/ba_data/audio/gong.ogg": "https://files.ballistica.net/cache/ba1/9d/98/fe9e5e31712035f2f821732fa426",
"assets/build/ba_data/audio/grandRompMusic.ogg": "https://files.ballistica.net/cache/ba1/44/9c/820f7ee74ad55c985a41824b629f",
"assets/build/ba_data/audio/gravelSkid.ogg": "https://files.ballistica.net/cache/ba1/91/a5/103c0131a043b57271a7dd498de1",
"assets/build/ba_data/audio/gunCocking.ogg": "https://files.ballistica.net/cache/ba1/8c/f3/c5e91220d25f23cbf61e8c2de65e",
"assets/build/ba_data/audio/healthPowerup.ogg": "https://files.ballistica.net/cache/ba1/21/79/faae5fb678a77a4514479213c5d2",
"assets/build/ba_data/audio/hiss.ogg": "https://files.ballistica.net/cache/ba1/14/a2/ad09926d21069aad7b56a55bb2b4",
"assets/build/ba_data/audio/impactHard.ogg": "https://files.ballistica.net/cache/ba1/7d/8b/8121a2270a1ba3efa3709a07c17a",
"assets/build/ba_data/audio/impactHard2.ogg": "https://files.ballistica.net/cache/ba1/08/1f/df44f42082c6f465ff5a78a832cc",
"assets/build/ba_data/audio/impactHard3.ogg": "https://files.ballistica.net/cache/ba1/78/6f/765aaeff1cd83131ebda60a2ed28",
"assets/build/ba_data/audio/impactMedium.ogg": "https://files.ballistica.net/cache/ba1/93/44/fc428d21a3e9a748389b78657ed0",
"assets/build/ba_data/audio/impactMedium2.ogg": "https://files.ballistica.net/cache/ba1/67/e0/6a5ba28a3750d940cfc85df9a48c",
"assets/build/ba_data/audio/jack01.ogg": "https://files.ballistica.net/cache/ba1/6f/d4/d026538f3457f2551e2b32f8a04e",
"assets/build/ba_data/audio/jack02.ogg": "https://files.ballistica.net/cache/ba1/b1/bb/f521552e2d5563d0002471f4934e",
"assets/build/ba_data/audio/jack03.ogg": "https://files.ballistica.net/cache/ba1/d8/27/5a3eb6f846ad3e1baf8f8bd180ab",
"assets/build/ba_data/audio/jack04.ogg": "https://files.ballistica.net/cache/ba1/82/e3/22c30cbca677c1146fd7e2a44402",
"assets/build/ba_data/audio/jack05.ogg": "https://files.ballistica.net/cache/ba1/03/88/f8a5726e7f05ca77fcb11458b84d",
"assets/build/ba_data/audio/jack06.ogg": "https://files.ballistica.net/cache/ba1/96/0f/3a11156fd9e49625ef93e5f1837c",
"assets/build/ba_data/audio/jackDeath01.ogg": "https://files.ballistica.net/cache/ba1/57/bd/dd8f71ede748e5b816fa0edacf2a",
"assets/build/ba_data/audio/jackFall01.ogg": "https://files.ballistica.net/cache/ba1/cd/da/511c6b136af4e42325894a17c974",
"assets/build/ba_data/audio/jackHit01.ogg": "https://files.ballistica.net/cache/ba1/10/17/301e483f642387bcf5b9a6cac455",
"assets/build/ba_data/audio/jackHit02.ogg": "https://files.ballistica.net/cache/ba1/a5/cb/47ed0c7ad9f1239fc0292eada391",
"assets/build/ba_data/audio/jackHit03.ogg": "https://files.ballistica.net/cache/ba1/bd/9c/dabb062bb53f6889324ed4e2648c",
"assets/build/ba_data/audio/jackHit04.ogg": "https://files.ballistica.net/cache/ba1/0f/66/4969ee4ffd6f93f8091d7e3c460e",
"assets/build/ba_data/audio/jackHit05.ogg": "https://files.ballistica.net/cache/ba1/e8/1e/f9df352de2a81c2c8e7cbb9d53f8",
"assets/build/ba_data/audio/jackHit06.ogg": "https://files.ballistica.net/cache/ba1/36/11/7f08ed2a79c0357346544a378b97",
"assets/build/ba_data/audio/jackHit07.ogg": "https://files.ballistica.net/cache/ba1/34/6b/d0de6621336d8a2dbac0b5a9cc64",
"assets/build/ba_data/audio/jumpsuit1.ogg": "https://files.ballistica.net/cache/ba1/2e/ba/5251d92cfc50e05891f49266a58c",
"assets/build/ba_data/audio/jumpsuit2.ogg": "https://files.ballistica.net/cache/ba1/85/ad/6318e0d7dfb4c562f0796d752ca1",
"assets/build/ba_data/audio/jumpsuit3.ogg": "https://files.ballistica.net/cache/ba1/b0/3a/b493714bed82a8447adcc12a305c",
"assets/build/ba_data/audio/jumpsuit4.ogg": "https://files.ballistica.net/cache/ba1/61/f6/07faf71815cc86137d4b0ee873bc",
"assets/build/ba_data/audio/jumpsuitDeath.ogg": "https://files.ballistica.net/cache/ba1/a0/ca/91cab7d426036018c9f695056001",
"assets/build/ba_data/audio/jumpsuitFall.ogg": "https://files.ballistica.net/cache/ba1/2e/89/3b4f798016588c8787598ba28104",
"assets/build/ba_data/audio/jumpsuitHit1.ogg": "https://files.ballistica.net/cache/ba1/54/3e/eabd980699d6a30c4f4a0d6ef239",
"assets/build/ba_data/audio/jumpsuitHit2.ogg": "https://files.ballistica.net/cache/ba1/19/e9/2077741fb34eeff9aa71f83dd098",
"assets/build/ba_data/audio/kronk1.ogg": "https://files.ballistica.net/cache/ba1/39/e8/d2b97739d30bb8933ee3fadbcbdd",
"assets/build/ba_data/audio/kronk10.ogg": "https://files.ballistica.net/cache/ba1/2e/f6/e062133061984ce247db1148235a",
"assets/build/ba_data/audio/kronk2.ogg": "https://files.ballistica.net/cache/ba1/59/47/70f7400429b641ff159b9be3bd68",
"assets/build/ba_data/audio/kronk3.ogg": "https://files.ballistica.net/cache/ba1/90/98/b950d293de08669c5eb37cf5b089",
"assets/build/ba_data/audio/kronk4.ogg": "https://files.ballistica.net/cache/ba1/1d/f4/47454ddc187e9c28f09b5f2641dd",
"assets/build/ba_data/audio/kronk5.ogg": "https://files.ballistica.net/cache/ba1/c9/0f/efacd82b0a3a1f7b7b0fa611ab24",
"assets/build/ba_data/audio/kronk6.ogg": "https://files.ballistica.net/cache/ba1/76/d6/8cd1f0b55d4297e24d94d86b27f9",
"assets/build/ba_data/audio/kronk7.ogg": "https://files.ballistica.net/cache/ba1/87/88/0cc4e3afd6735e62009a1b7a559f",
"assets/build/ba_data/audio/kronk8.ogg": "https://files.ballistica.net/cache/ba1/03/43/00789923ce5d554b1fc59dc29efe",
"assets/build/ba_data/audio/kronk9.ogg": "https://files.ballistica.net/cache/ba1/9e/37/905f2b0bf9b62aac66370e22171a",
"assets/build/ba_data/audio/kronkDeath.ogg": "https://files.ballistica.net/cache/ba1/84/93/9d86402d4c414f7f8e5cbb84e7da",
"assets/build/ba_data/audio/kronkFall.ogg": "https://files.ballistica.net/cache/ba1/39/5e/bee74f268c641c327fe574ff96a1",
"assets/build/ba_data/audio/laser.ogg": "https://files.ballistica.net/cache/ba1/ba/11/d9ea29ec409d58ef9933051f00a3",
"assets/build/ba_data/audio/laserReverse.ogg": "https://files.ballistica.net/cache/ba1/4d/6c/ea540847197dd3817f00836b7c47",
"assets/build/ba_data/audio/mel01.ogg": "https://files.ballistica.net/cache/ba1/65/1f/f146b8e3a3854caf4d3e9dfb5907",
"assets/build/ba_data/audio/mel02.ogg": "https://files.ballistica.net/cache/ba1/a9/06/26c796767095b7cae2b822807736",
"assets/build/ba_data/audio/mel03.ogg": "https://files.ballistica.net/cache/ba1/fe/d5/6f54d408cc2a4c9e875d72a2471f",
"assets/build/ba_data/audio/mel04.ogg": "https://files.ballistica.net/cache/ba1/6c/70/91661316e90b2fc681b248196415",
"assets/build/ba_data/audio/mel05.ogg": "https://files.ballistica.net/cache/ba1/aa/c2/2d1a6e91f87196ab0e0582b34f4d",
"assets/build/ba_data/audio/mel06.ogg": "https://files.ballistica.net/cache/ba1/90/a4/cfcf8f764e5e6f63869f01e9240b",
"assets/build/ba_data/audio/mel07.ogg": "https://files.ballistica.net/cache/ba1/1c/7b/c8a607b89f95e7c3c91a9a6dbd1f",
"assets/build/ba_data/audio/mel08.ogg": "https://files.ballistica.net/cache/ba1/d3/fb/ec4085912068c8890bfdaf5f6f41",
"assets/build/ba_data/audio/mel09.ogg": "https://files.ballistica.net/cache/ba1/99/60/1ba62554da26788072721151c7ca",
"assets/build/ba_data/audio/mel10.ogg": "https://files.ballistica.net/cache/ba1/38/ed/d131624611fd8bf12faf95ad1bfe",
"assets/build/ba_data/audio/melDeath01.ogg": "https://files.ballistica.net/cache/ba1/56/55/cfcbc1b20f357843d1efa62bdc17",
"assets/build/ba_data/audio/melFall01.ogg": "https://files.ballistica.net/cache/ba1/82/e6/9ce3ae6f486f6f951e97c9eac869",
"assets/build/ba_data/audio/menuMusic.ogg": "https://files.ballistica.net/cache/ba1/2f/04/3b69056ee929761c7e7e702db2c1",
"assets/build/ba_data/audio/metalHit.ogg": "https://files.ballistica.net/cache/ba1/9c/30/a61de1a6d1787ab90b2647e6d8e2",
"assets/build/ba_data/audio/metalSkid.ogg": "https://files.ballistica.net/cache/ba1/03/77/fe701a38c7c448b6c29c44df1be3",
"assets/build/ba_data/audio/ninjaAttack1.ogg": "https://files.ballistica.net/cache/ba1/3e/f6/b245f532ead500368f0b413177ce",
"assets/build/ba_data/audio/ninjaAttack2.ogg": "https://files.ballistica.net/cache/ba1/7b/a9/47dfcb386fc92e07af0badffebe2",
"assets/build/ba_data/audio/ninjaAttack3.ogg": "https://files.ballistica.net/cache/ba1/8c/87/d4e6698efa137c523ffe9843d46f",
"assets/build/ba_data/audio/ninjaAttack4.ogg": "https://files.ballistica.net/cache/ba1/a3/35/5f1533eef06b6e0c6d4cce1bb437",
"assets/build/ba_data/audio/ninjaAttack5.ogg": "https://files.ballistica.net/cache/ba1/98/72/c5ff77cf253c0141531c791b2fd3",
"assets/build/ba_data/audio/ninjaAttack6.ogg": "https://files.ballistica.net/cache/ba1/87/ef/abcb38ba5f79212d975f1e8660f3",
"assets/build/ba_data/audio/ninjaAttack7.ogg": "https://files.ballistica.net/cache/ba1/3f/6f/94d48a93f1ecb66f786c27a9fd63",
"assets/build/ba_data/audio/ninjaDeath1.ogg": "https://files.ballistica.net/cache/ba1/62/63/10c7a4204a1383b8330bae91af14",
"assets/build/ba_data/audio/ninjaFall1.ogg": "https://files.ballistica.net/cache/ba1/6d/38/7067e8b67e42646f0a3f6fea0b25",
"assets/build/ba_data/audio/ninjaHit1.ogg": "https://files.ballistica.net/cache/ba1/52/dd/c411d8585d17f542075f279e1b83",
"assets/build/ba_data/audio/ninjaHit2.ogg": "https://files.ballistica.net/cache/ba1/18/ef/e6df5814069eb171a43014dda531",
"assets/build/ba_data/audio/ninjaHit3.ogg": "https://files.ballistica.net/cache/ba1/8d/94/751982314c25413ab46b4ba4552c",
"assets/build/ba_data/audio/ninjaHit4.ogg": "https://files.ballistica.net/cache/ba1/fc/45/513450f5f446ce5c256828b79785",
"assets/build/ba_data/audio/ninjaHit5.ogg": "https://files.ballistica.net/cache/ba1/13/35/a55d46a38eacd2ceb43ee8703b73",
"assets/build/ba_data/audio/ninjaHit6.ogg": "https://files.ballistica.net/cache/ba1/49/27/eb032a6501af3b4d95d3fa1f8dda",
"assets/build/ba_data/audio/ninjaHit7.ogg": "https://files.ballistica.net/cache/ba1/e9/ed/bde7ba9d85250298b1002f23fa9e",
"assets/build/ba_data/audio/ninjaHit8.ogg": "https://files.ballistica.net/cache/ba1/c4/0f/b4be18ea1ccd5c8d9a2bc784f19f",
"assets/build/ba_data/audio/oldLady1.ogg": "https://files.ballistica.net/cache/ba1/01/d5/d07ff67ffc9ed9c30433a57a722d",
"assets/build/ba_data/audio/oldLady2.ogg": "https://files.ballistica.net/cache/ba1/8b/2a/2c0c10526294afbac2ed5fdbe7d5",
"assets/build/ba_data/audio/oldLady3.ogg": "https://files.ballistica.net/cache/ba1/46/d7/a64bb5019b01a9bf8a3a71c32d85",
"assets/build/ba_data/audio/oldLady4.ogg": "https://files.ballistica.net/cache/ba1/63/64/a98e6bbeffe5a8947a7320ca06c5",
"assets/build/ba_data/audio/oldLadyDeath.ogg": "https://files.ballistica.net/cache/ba1/a0/ca/4a4c7cc453d0f254dfeffdaad436",
"assets/build/ba_data/audio/oldLadyFall.ogg": "https://files.ballistica.net/cache/ba1/92/41/b3d47aa08e285a464ab1d81e02b5",
"assets/build/ba_data/audio/oldLadyHit1.ogg": "https://files.ballistica.net/cache/ba1/68/74/6019b76e9d36f153ead330ebbdee",
"assets/build/ba_data/audio/oldLadyHit2.ogg": "https://files.ballistica.net/cache/ba1/06/2c/3807ed553aacf8ffcac05bc334a9",
"assets/build/ba_data/audio/ooh.ogg": "https://files.ballistica.net/cache/ba1/75/78/b05997e40ea9327283bfbcd73d0f",
"assets/build/ba_data/audio/operaSinger1.ogg": "https://files.ballistica.net/cache/ba1/67/b4/456ca84157861e1a01552f1bbb60",
"assets/build/ba_data/audio/operaSinger2.ogg": "https://files.ballistica.net/cache/ba1/7f/e1/aa3e383b835e6caabc52e92c0f2e",
"assets/build/ba_data/audio/operaSinger3.ogg": "https://files.ballistica.net/cache/ba1/2f/3b/8f41cca76f4d874df8c418a14844",
"assets/build/ba_data/audio/operaSinger4.ogg": "https://files.ballistica.net/cache/ba1/86/0c/4814731188a54fc6829eb3d4987e",
"assets/build/ba_data/audio/operaSingerDeath.ogg": "https://files.ballistica.net/cache/ba1/37/6f/82c8fe95d70eccacd9c5d95f54a5",
"assets/build/ba_data/audio/operaSingerFall.ogg": "https://files.ballistica.net/cache/ba1/c0/d4/6f960ef15bbded8ce6f2b5b6657f",
"assets/build/ba_data/audio/operaSingerHit1.ogg": "https://files.ballistica.net/cache/ba1/a8/c0/c1aea91573c7902322105d920502",
"assets/build/ba_data/audio/operaSingerHit2.ogg": "https://files.ballistica.net/cache/ba1/6a/b8/0ed146abd109205008d9badea4ae",
"assets/build/ba_data/audio/orchestraHit.ogg": "https://files.ballistica.net/cache/ba1/e2/1f/ece696b97992ca2193c352f3da84",
"assets/build/ba_data/audio/orchestraHit2.ogg": "https://files.ballistica.net/cache/ba1/93/23/089101e0936aa2f05a3aaee20f8e",
"assets/build/ba_data/audio/orchestraHit3.ogg": "https://files.ballistica.net/cache/ba1/6f/88/41a8b64754c979c2f608558b2205",
"assets/build/ba_data/audio/orchestraHit4.ogg": "https://files.ballistica.net/cache/ba1/0d/52/4396bd2e95954bbc3c88aba19f5f",
"assets/build/ba_data/audio/orchestraHitBig1.ogg": "https://files.ballistica.net/cache/ba1/cb/48/deb5a4eeeaa0e9c14456f66d2fe8",
"assets/build/ba_data/audio/orchestraHitBig2.ogg": "https://files.ballistica.net/cache/ba1/2a/c4/84c2e64feffba098cd1dacb59c1c",
"assets/build/ba_data/audio/penguin1.ogg": "https://files.ballistica.net/cache/ba1/66/e4/d940e0bcae6a7a658aacf15ee8c6",
"assets/build/ba_data/audio/penguin2.ogg": "https://files.ballistica.net/cache/ba1/5e/dc/54dd8785e0236478f219e15a2b32",
"assets/build/ba_data/audio/penguin3.ogg": "https://files.ballistica.net/cache/ba1/d7/6e/fab15b63c2e8dafef1f523af9448",
"assets/build/ba_data/audio/penguin4.ogg": "https://files.ballistica.net/cache/ba1/df/72/f49eb5520e1275556a309925ca2f",
"assets/build/ba_data/audio/penguinDeath.ogg": "https://files.ballistica.net/cache/ba1/31/a8/f414335a2e8f2f7f11d927740993",
"assets/build/ba_data/audio/penguinFall.ogg": "https://files.ballistica.net/cache/ba1/75/ac/6673cf05b554f93e4f80e5a1f032",
"assets/build/ba_data/audio/penguinHit1.ogg": "https://files.ballistica.net/cache/ba1/fb/80/d751a93245e5a81f2d7122b10601",
"assets/build/ba_data/audio/penguinHit2.ogg": "https://files.ballistica.net/cache/ba1/37/07/219f073f5e8042795662c37e27c2",
"assets/build/ba_data/audio/pixie1.ogg": "https://files.ballistica.net/cache/ba1/a1/ba/0e576188e12d5d3d90dbe2ba4aa2",
"assets/build/ba_data/audio/pixie2.ogg": "https://files.ballistica.net/cache/ba1/a2/a8/df2edb0fcddee0a292b116f6511a",
"assets/build/ba_data/audio/pixie3.ogg": "https://files.ballistica.net/cache/ba1/4e/f6/74bc253a5cc01d681e4ad8d38337",
"assets/build/ba_data/audio/pixie4.ogg": "https://files.ballistica.net/cache/ba1/da/7e/bf6c1a336eab325cc602a1876a23",
"assets/build/ba_data/audio/pixieDeath.ogg": "https://files.ballistica.net/cache/ba1/62/46/e12933d09d0d589c7c122c9b8fa8",
"assets/build/ba_data/audio/pixieFall.ogg": "https://files.ballistica.net/cache/ba1/0c/e1/b1a37fc2a365f693aa40a756c235",
"assets/build/ba_data/audio/pixieHit1.ogg": "https://files.ballistica.net/cache/ba1/2a/7a/12e4dafb36ffef321e55f943a12c",
"assets/build/ba_data/audio/pixieHit2.ogg": "https://files.ballistica.net/cache/ba1/61/1c/93fb0399ad2bcab8ccdb5e145633",
"assets/build/ba_data/audio/playerDeath.ogg": "https://files.ballistica.net/cache/ba1/26/47/b948161697a84fb0bce339f3ad71",
"assets/build/ba_data/audio/playerLeft.ogg": "https://files.ballistica.net/cache/ba1/bc/9d/da4df4b55e84fe9eb05509049f21",
"assets/build/ba_data/audio/pop01.ogg": "https://files.ballistica.net/cache/ba1/e0/ca/fc13159fce5ef09dc2ad1db9645d",
"assets/build/ba_data/audio/powerdown01.ogg": "https://files.ballistica.net/cache/ba1/ac/8f/4d4e570ab6238879232242bd2367",
"assets/build/ba_data/audio/powerup01.ogg": "https://files.ballistica.net/cache/ba1/16/7a/93da7e6859d880331e589732cf70",
"assets/build/ba_data/audio/punch01.ogg": "https://files.ballistica.net/cache/ba1/bb/fd/85fbab1939dea07583aae2c9f43b",
"assets/build/ba_data/audio/punchStrong01.ogg": "https://files.ballistica.net/cache/ba1/46/1b/62bc8d51c3672bacb9c2b99ce66f",
"assets/build/ba_data/audio/punchStrong02.ogg": "https://files.ballistica.net/cache/ba1/d5/f7/feb06da4c021a2c81a505593a7f4",
"assets/build/ba_data/audio/punchSwish.ogg": "https://files.ballistica.net/cache/ba1/fa/47/9494fe756a1f7cde27deab651a2d",
"assets/build/ba_data/audio/punchWeak01.ogg": "https://files.ballistica.net/cache/ba1/8c/ac/4055d2df143632938148dd369b40",
"assets/build/ba_data/audio/raceBeep1.ogg": "https://files.ballistica.net/cache/ba1/1c/52/61b720a805db063223eb5f9572f4",
"assets/build/ba_data/audio/raceBeep2.ogg": "https://files.ballistica.net/cache/ba1/c7/b3/7c33fec0f23065a56d7bdd082af8",
"assets/build/ba_data/audio/refWhistle.ogg": "https://files.ballistica.net/cache/ba1/9b/dd/da98b9d3efafaf2655407f64a09c",
"assets/build/ba_data/audio/robot1.ogg": "https://files.ballistica.net/cache/ba1/c4/f3/46c5b5a8d60f1ee7e5836ddf3baf",
"assets/build/ba_data/audio/robot2.ogg": "https://files.ballistica.net/cache/ba1/6c/d5/59c4c07ec9edec23590bcc9aa7d7",
"assets/build/ba_data/audio/robot3.ogg": "https://files.ballistica.net/cache/ba1/10/98/27748b76c889f5b21def4c702341",
"assets/build/ba_data/audio/robot4.ogg": "https://files.ballistica.net/cache/ba1/b4/32/0bf631e348c14b41a64190e8376a",
"assets/build/ba_data/audio/robotDeath.ogg": "https://files.ballistica.net/cache/ba1/65/3e/599289f5b9d08d02607f26f9c42c",
"assets/build/ba_data/audio/robotFall.ogg": "https://files.ballistica.net/cache/ba1/35/cc/9693e8fa39a5ad2a245f3a848e72",
"assets/build/ba_data/audio/robotHit1.ogg": "https://files.ballistica.net/cache/ba1/70/a9/07a5eb870bfd24143a5a1dae6b11",
"assets/build/ba_data/audio/robotHit2.ogg": "https://files.ballistica.net/cache/ba1/60/79/490c8f75cce45183a66357d30a85",
"assets/build/ba_data/audio/runAwayMusic.ogg": "https://files.ballistica.net/cache/ba1/28/8d/db39413af847b4aba765938a439c",
"assets/build/ba_data/audio/santa01.ogg": "https://files.ballistica.net/cache/ba1/8f/25/7b73c6cb3a67dd4d0c537a8ff486",
"assets/build/ba_data/audio/santa02.ogg": "https://files.ballistica.net/cache/ba1/6b/62/d9acb101fc2ae91cce4ebf21dea7",
"assets/build/ba_data/audio/santa03.ogg": "https://files.ballistica.net/cache/ba1/1c/ed/a69839caf83c6f2678b194aa5bb2",
"assets/build/ba_data/audio/santa04.ogg": "https://files.ballistica.net/cache/ba1/c9/46/1e88517f694e317b2e19574c6817",
"assets/build/ba_data/audio/santa05.ogg": "https://files.ballistica.net/cache/ba1/90/5f/dcb00f73be3f7ba6a3d2253f29db",
"assets/build/ba_data/audio/santaDeath.ogg": "https://files.ballistica.net/cache/ba1/64/4b/3d8d95da0c30e91db75a67726e8b",
"assets/build/ba_data/audio/santaFall.ogg": "https://files.ballistica.net/cache/ba1/13/45/ec999a23e68ad3b3ca731e8ee004",
"assets/build/ba_data/audio/santaHit01.ogg": "https://files.ballistica.net/cache/ba1/7c/35/1c404fe708f33b7ff8fa9aaf1d6b",
"assets/build/ba_data/audio/santaHit02.ogg": "https://files.ballistica.net/cache/ba1/b7/7c/05b6af01635a185c7ef6eda204ef",
"assets/build/ba_data/audio/santaHit03.ogg": "https://files.ballistica.net/cache/ba1/bb/3a/faf8638851148f5a1c6759afd9f7",
"assets/build/ba_data/audio/santaHit04.ogg": "https://files.ballistica.net/cache/ba1/cd/49/6722b707a2863cb28f18e4e4d359",
"assets/build/ba_data/audio/scamper01.ogg": "https://files.ballistica.net/cache/ba1/3a/50/6874379b5acfa4333fac8374f388",
"assets/build/ba_data/audio/scaryMusic.ogg": "https://files.ballistica.net/cache/ba1/de/62/d51342211c0ee9ffc15753a0dc38",
"assets/build/ba_data/audio/score.ogg": "https://files.ballistica.net/cache/ba1/35/63/59c439ce3ae53448b69e65b12f01",
"assets/build/ba_data/audio/scoreHit01.ogg": "https://files.ballistica.net/cache/ba1/ae/3d/4817efe2e3397780a333b75df614",
"assets/build/ba_data/audio/scoreHit02.ogg": "https://files.ballistica.net/cache/ba1/66/3e/db8772a6b9f0fd0f56072fa1ec65",
"assets/build/ba_data/audio/scoreIncrease.ogg": "https://files.ballistica.net/cache/ba1/a3/d4/2d4f01ac1e805a91f2f6232a0047",
"assets/build/ba_data/audio/scoresEpicMusic.ogg": "https://files.ballistica.net/cache/ba1/7a/dd/61fb2430a8f22259f2cbe4558766",
"assets/build/ba_data/audio/shatter.ogg": "https://files.ballistica.net/cache/ba1/d1/d6/50f5299a21f7f88e35871be01342",
"assets/build/ba_data/audio/shieldDown.ogg": "https://files.ballistica.net/cache/ba1/4f/31/da9f37085fa732a001dc23eeb32b",
"assets/build/ba_data/audio/shieldHit.ogg": "https://files.ballistica.net/cache/ba1/c1/16/915c9c8ea1d8996fd8fb7d18ab9d",
"assets/build/ba_data/audio/shieldUp.ogg": "https://files.ballistica.net/cache/ba1/3f/d1/096975c0e59df14948b0a3595c3a",
"assets/build/ba_data/audio/skid01.ogg": "https://files.ballistica.net/cache/ba1/f0/f8/9e6e4378363b6e8eec747cd592e6",
"assets/build/ba_data/audio/slowEpicMusic.ogg": "https://files.ballistica.net/cache/ba1/e0/76/3d356a9f7c2ecf52144adcd93c3e",
"assets/build/ba_data/audio/sparkle01.ogg": "https://files.ballistica.net/cache/ba1/00/23/d33bc638bb97885470d8d1dceff4",
"assets/build/ba_data/audio/sparkle02.ogg": "https://files.ballistica.net/cache/ba1/0f/97/6bf234b8297dacb24d16f3fc6835",
"assets/build/ba_data/audio/sparkle03.ogg": "https://files.ballistica.net/cache/ba1/7b/67/f32943885c7b4e42cd71c950c094",
"assets/build/ba_data/audio/spawn.ogg": "https://files.ballistica.net/cache/ba1/5b/e1/7976a3409551f06b465dbf9afc92",
"assets/build/ba_data/audio/spazAttack01.ogg": "https://files.ballistica.net/cache/ba1/ce/c1/a0fe34d38d134a260e8215117be5",
"assets/build/ba_data/audio/spazAttack02.ogg": "https://files.ballistica.net/cache/ba1/5b/ab/f5708a7196cd7c1d3c5856c49077",
"assets/build/ba_data/audio/spazAttack03.ogg": "https://files.ballistica.net/cache/ba1/02/b9/ced762c00a80b55f85e5a537694d",
"assets/build/ba_data/audio/spazAttack04.ogg": "https://files.ballistica.net/cache/ba1/58/09/ffc7f2269fccb7d6ba3a2d4fa70e",
"assets/build/ba_data/audio/spazDeath01.ogg": "https://files.ballistica.net/cache/ba1/64/75/818509a97557a1efed0d391c7d26",
"assets/build/ba_data/audio/spazEff.ogg": "https://files.ballistica.net/cache/ba1/bd/70/525e9bd572821a59886d89790f48",
"assets/build/ba_data/audio/spazFall01.ogg": "https://files.ballistica.net/cache/ba1/f0/4b/07e339002b262a7ffe2427a749e3",
"assets/build/ba_data/audio/spazImpact01.ogg": "https://files.ballistica.net/cache/ba1/0e/33/de9d8f2006bdfba05d4daa4c43bc",
"assets/build/ba_data/audio/spazImpact02.ogg": "https://files.ballistica.net/cache/ba1/19/ea/6d27a7931ed4b60f6de393422046",
"assets/build/ba_data/audio/spazImpact03.ogg": "https://files.ballistica.net/cache/ba1/fd/7c/abc4d0b8350f263675fce1719815",
"assets/build/ba_data/audio/spazImpact04.ogg": "https://files.ballistica.net/cache/ba1/0e/04/29b57759816c1dc80f0fcc8dbdf5",
"assets/build/ba_data/audio/spazJump01.ogg": "https://files.ballistica.net/cache/ba1/0a/74/02bcb3ccfdc45548d2d6328d5762",
"assets/build/ba_data/audio/spazJump02.ogg": "https://files.ballistica.net/cache/ba1/87/ae/f80c533dedc2a5048c31c7fd05a5",
"assets/build/ba_data/audio/spazJump03.ogg": "https://files.ballistica.net/cache/ba1/8d/f3/d89923c4894e8fe5a144f3dac2b8",
"assets/build/ba_data/audio/spazJump04.ogg": "https://files.ballistica.net/cache/ba1/01/76/c82cf77f0ea683ced0dce96a6590",
"assets/build/ba_data/audio/spazOw.ogg": "https://files.ballistica.net/cache/ba1/81/30/c272cef86432c32683fff2b6a856",
"assets/build/ba_data/audio/spazPickup01.ogg": "https://files.ballistica.net/cache/ba1/0f/3f/49392c99fe3f5029086a1cc80d8a",
"assets/build/ba_data/audio/spazScream01.ogg": "https://files.ballistica.net/cache/ba1/ac/99/e38ffb38dcac926ebc9666e6687a",
"assets/build/ba_data/audio/splatter.ogg": "https://files.ballistica.net/cache/ba1/65/0e/95216b45b9fb7e1fcf29753a3b86",
"assets/build/ba_data/audio/sportsMusic.ogg": "https://files.ballistica.net/cache/ba1/b8/be/bded110195557ffeb9928f8f35a8",
"assets/build/ba_data/audio/stickyImpact.ogg": "https://files.ballistica.net/cache/ba1/7f/96/aca2db315454d319400d505c84bd",
"assets/build/ba_data/audio/superPunch.ogg": "https://files.ballistica.net/cache/ba1/68/b1/3857d2b85c2137dfc71c59d99866",
"assets/build/ba_data/audio/superhero1.ogg": "https://files.ballistica.net/cache/ba1/f8/be/d5ddef17888c0f0918a6ca5f9fed",
"assets/build/ba_data/audio/superhero2.ogg": "https://files.ballistica.net/cache/ba1/c5/88/fa61647f14b026cdb0c710a6a241",
"assets/build/ba_data/audio/superhero3.ogg": "https://files.ballistica.net/cache/ba1/a6/1a/a0fa6c752ceacbc3d581191bea79",
"assets/build/ba_data/audio/superhero4.ogg": "https://files.ballistica.net/cache/ba1/e8/a8/ffc0743aa669e24bb7c856359ff1",
"assets/build/ba_data/audio/superheroDeath.ogg": "https://files.ballistica.net/cache/ba1/98/00/7fa8feb367547d7b4053d6d6f8e8",
"assets/build/ba_data/audio/superheroFall.ogg": "https://files.ballistica.net/cache/ba1/38/46/dd8019ab7a705500e32609f8d240",
"assets/build/ba_data/audio/superheroHit1.ogg": "https://files.ballistica.net/cache/ba1/d7/64/04ae978100fa9d92f94b3e474c36",
"assets/build/ba_data/audio/superheroHit2.ogg": "https://files.ballistica.net/cache/ba1/71/d2/d20bcca0587ea75ec510b9e9d9a3",
"assets/build/ba_data/audio/survivalMusic.ogg": "https://files.ballistica.net/cache/ba1/7b/3a/afefd67ab21251571721a84d5631",
"assets/build/ba_data/audio/swip.ogg": "https://files.ballistica.net/cache/ba1/a5/fc/7c9573cac08082db1eb1a1780be6",
"assets/build/ba_data/audio/swip2.ogg": "https://files.ballistica.net/cache/ba1/c0/8a/f4acc9197c51dd87bdcd649979f7",
"assets/build/ba_data/audio/swish.ogg": "https://files.ballistica.net/cache/ba1/5e/ef/8085f12f781b3fd1a83e7a3f9ec1",
"assets/build/ba_data/audio/swish2.ogg": "https://files.ballistica.net/cache/ba1/e2/42/6242489e58bd314faddbc2cdfa39",
"assets/build/ba_data/audio/swish3.ogg": "https://files.ballistica.net/cache/ba1/cd/00/dc59c4e85428742a3219d790862c",
"assets/build/ba_data/audio/tap.ogg": "https://files.ballistica.net/cache/ba1/07/fb/512704ee3acc4f54dbbe335a0b92",
"assets/build/ba_data/audio/technoHit01.ogg": "https://files.ballistica.net/cache/ba1/d7/3d/c4bd72b56365c29c2ff7eaf237f7",
"assets/build/ba_data/audio/tick.ogg": "https://files.ballistica.net/cache/ba1/e7/22/9effe1e9201ca6f787536c2cc7d1",
"assets/build/ba_data/audio/ticking.ogg": "https://files.ballistica.net/cache/ba1/01/a7/e9b2614da4bb07ad3c54d81284fe",
"assets/build/ba_data/audio/tickingCrazy.ogg": "https://files.ballistica.net/cache/ba1/49/b1/b258d1b3223fe5c2f76590cc3609",
"assets/build/ba_data/audio/toTheDeathMusic.ogg": "https://files.ballistica.net/cache/ba1/5d/e8/f388a2c241952af159074ec131a5",
"assets/build/ba_data/audio/trashRummage.ogg": "https://files.ballistica.net/cache/ba1/9e/a1/56f36558300299849f1141bbe313",
"assets/build/ba_data/audio/victoryMusic.ogg": "https://files.ballistica.net/cache/ba1/b4/3f/3910943af8d4303708c75cbfe621",
"assets/build/ba_data/audio/warnBeep.ogg": "https://files.ballistica.net/cache/ba1/dc/37/9c623b28aae041ca19ba3dc97f6a",
"assets/build/ba_data/audio/warnBeeps.ogg": "https://files.ballistica.net/cache/ba1/0e/6c/ebc2183df283709a0198849fd7c0",
"assets/build/ba_data/audio/warrior1.ogg": "https://files.ballistica.net/cache/ba1/ab/cb/7e57a8733470f535142015d9582f",
"assets/build/ba_data/audio/warrior2.ogg": "https://files.ballistica.net/cache/ba1/35/65/14173145d20eaf84ad79fb0f1a69",
"assets/build/ba_data/audio/warrior3.ogg": "https://files.ballistica.net/cache/ba1/6a/1c/4d238beefe81e1d44ef8eef00de0",
"assets/build/ba_data/audio/warrior4.ogg": "https://files.ballistica.net/cache/ba1/42/07/6d08d278a613b85adde7b2c8b8f8",
"assets/build/ba_data/audio/warriorDeath.ogg": "https://files.ballistica.net/cache/ba1/62/18/36598cfbd77bc73bd6bb4b02d0ac",
"assets/build/ba_data/audio/warriorFall.ogg": "https://files.ballistica.net/cache/ba1/b8/f1/c9b2fe1babd2c3027c2c6b955dd9",
"assets/build/ba_data/audio/warriorHit1.ogg": "https://files.ballistica.net/cache/ba1/66/88/1aba5328534ea0b21db7e0c35a97",
"assets/build/ba_data/audio/warriorHit2.ogg": "https://files.ballistica.net/cache/ba1/ff/cb/42e31f8089ad1760d7aad0d7ff7f",
"assets/build/ba_data/audio/whenJohnnyComesMarchingHomeMusic.ogg": "https://files.ballistica.net/cache/ba1/fb/d9/c6925d7fb50c229857c172478bc3",
"assets/build/ba_data/audio/witch1.ogg": "https://files.ballistica.net/cache/ba1/5c/03/9f32f1bda3d7fa8ded269938d5fc",
"assets/build/ba_data/audio/witch2.ogg": "https://files.ballistica.net/cache/ba1/3e/f6/3eb43eba873b08f776cd54ba4ecd",
"assets/build/ba_data/audio/witch3.ogg": "https://files.ballistica.net/cache/ba1/fc/38/74f15bb395ecd29b50ef4cacbd3b",
"assets/build/ba_data/audio/witch4.ogg": "https://files.ballistica.net/cache/ba1/8f/71/87c58a955524e8f7ef19db0b2c19",
"assets/build/ba_data/audio/witchDeath.ogg": "https://files.ballistica.net/cache/ba1/63/b7/34d892c9382fcca2143f47360681",
"assets/build/ba_data/audio/witchFall.ogg": "https://files.ballistica.net/cache/ba1/ca/21/e880c95abe23462d31027d396d17",
"assets/build/ba_data/audio/witchHit1.ogg": "https://files.ballistica.net/cache/ba1/28/ff/eda01ba4d712ed8f87ad41689eb5",
"assets/build/ba_data/audio/witchHit2.ogg": "https://files.ballistica.net/cache/ba1/7f/9b/9ae61751b71e2b3cd6e380351afe",
"assets/build/ba_data/audio/wizard1.ogg": "https://files.ballistica.net/cache/ba1/1d/54/3f31d7d0e0b9c0e7514c8d7b07d6",
"assets/build/ba_data/audio/wizard2.ogg": "https://files.ballistica.net/cache/ba1/fc/c5/02945950ad0e595983b9042a7fd2",
"assets/build/ba_data/audio/wizard3.ogg": "https://files.ballistica.net/cache/ba1/c3/64/86c3743515757904d14676aecadc",
"assets/build/ba_data/audio/wizard4.ogg": "https://files.ballistica.net/cache/ba1/92/6e/1274261230bf73320ac2b8ea32a2",
"assets/build/ba_data/audio/wizardDeath.ogg": "https://files.ballistica.net/cache/ba1/68/57/dfa7a98252eeb017ba95022a71d4",
"assets/build/ba_data/audio/wizardFall.ogg": "https://files.ballistica.net/cache/ba1/72/7a/6fd39cfd7cf25152fe267061b6e6",
"assets/build/ba_data/audio/wizardHit1.ogg": "https://files.ballistica.net/cache/ba1/d8/47/8774de373d5716f8b1def11323ba",
"assets/build/ba_data/audio/wizardHit2.ogg": "https://files.ballistica.net/cache/ba1/6e/6d/14771c5d4885856765fef6668ffd",
"assets/build/ba_data/audio/woodDebrisFall.ogg": "https://files.ballistica.net/cache/ba1/79/94/e96e205a438bcf6ea27829616eeb",
"assets/build/ba_data/audio/wrestler1.ogg": "https://files.ballistica.net/cache/ba1/2f/eb/15d8e43ce34d1677a25bf3870d30",
"assets/build/ba_data/audio/wrestler2.ogg": "https://files.ballistica.net/cache/ba1/f7/9a/27c12f0bc136c5ff5d361a855b54",
"assets/build/ba_data/audio/wrestler3.ogg": "https://files.ballistica.net/cache/ba1/22/cb/8a7866702f5e95d18287ad3a3b3c",
"assets/build/ba_data/audio/wrestler4.ogg": "https://files.ballistica.net/cache/ba1/08/9f/90516d853c3e62ecd1dcb1d9f0ff",
"assets/build/ba_data/audio/wrestlerDeath.ogg": "https://files.ballistica.net/cache/ba1/dc/a6/8019396b91152cab4db09b0a7ec8",
"assets/build/ba_data/audio/wrestlerFall.ogg": "https://files.ballistica.net/cache/ba1/49/16/5edc44a1ed74fc37b57c7473b1f6",
"assets/build/ba_data/audio/wrestlerHit1.ogg": "https://files.ballistica.net/cache/ba1/3d/fb/6b2741940714e3f5a564c55ccd6a",
"assets/build/ba_data/audio/wrestlerHit2.ogg": "https://files.ballistica.net/cache/ba1/4c/e3/f398eae1b37afbec4b42cd88c90c",
"assets/build/ba_data/audio/zoeAttack01.ogg": "https://files.ballistica.net/cache/ba1/37/04/5a12395a7df33f69d7bf874f5054",
"assets/build/ba_data/audio/zoeAttack02.ogg": "https://files.ballistica.net/cache/ba1/f3/b1/ac78bfad015ebdc003a408bbf1c0",
"assets/build/ba_data/audio/zoeAttack03.ogg": "https://files.ballistica.net/cache/ba1/97/e0/2f82bf9878472e3e5535a29f6b57",
"assets/build/ba_data/audio/zoeAttack04.ogg": "https://files.ballistica.net/cache/ba1/2c/6a/da814c4c37dd0b5d0ff3564be852",
"assets/build/ba_data/audio/zoeDeath01.ogg": "https://files.ballistica.net/cache/ba1/0b/3f/d197dafe8e65bc9ad44227d76595",
"assets/build/ba_data/audio/zoeEff.ogg": "https://files.ballistica.net/cache/ba1/d0/63/161c6c7139a432a6548657b2cbf8",
"assets/build/ba_data/audio/zoeFall01.ogg": "https://files.ballistica.net/cache/ba1/15/9b/bc1e6d1c46984ff15d8c7c7be553",
"assets/build/ba_data/audio/zoeImpact01.ogg": "https://files.ballistica.net/cache/ba1/14/ab/e0ffc2ee28d047d498e7fb9d9a29",
"assets/build/ba_data/audio/zoeImpact02.ogg": "https://files.ballistica.net/cache/ba1/46/06/2da242b286aa30d24b1a51c48a1e",
"assets/build/ba_data/audio/zoeImpact03.ogg": "https://files.ballistica.net/cache/ba1/5b/bb/820dca4d4ebd863aacab96739197",
"assets/build/ba_data/audio/zoeImpact04.ogg": "https://files.ballistica.net/cache/ba1/2e/83/10966e237b36b0726962e7511789",
"assets/build/ba_data/audio/zoeJump01.ogg": "https://files.ballistica.net/cache/ba1/51/b0/6912fe028eb5e787e8b2e2451aa7",
"assets/build/ba_data/audio/zoeJump02.ogg": "https://files.ballistica.net/cache/ba1/ad/3b/1ea95c528b3ac203407c71b6c250",
"assets/build/ba_data/audio/zoeJump03.ogg": "https://files.ballistica.net/cache/ba1/88/2d/857625213c26f31ca53625228ac5",
"assets/build/ba_data/audio/zoeOw.ogg": "https://files.ballistica.net/cache/ba1/dc/d2/160fc27fcaff10793327ac2c70fd",
"assets/build/ba_data/audio/zoePickup01.ogg": "https://files.ballistica.net/cache/ba1/11/7a/87d6bca0acfb877fd4fd8fe3a598",
"assets/build/ba_data/audio/zoeScream01.ogg": "https://files.ballistica.net/cache/ba1/44/f5/c943c9075abb3e1835d2408a1ef8",
"assets/build/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/bd/6c/48e30d0a2215958f8ae1a02805ba",
"assets/build/ba_data/data/languages/arabic.json": "https://files.ballistica.net/cache/ba1/ca/75/3de74bd6e498113b99bbf9eda645",
"assets/build/ba_data/data/languages/belarussian.json": "https://files.ballistica.net/cache/ba1/61/03/89070ca765e06da3a419a579f503",
@ -3971,50 +3971,50 @@
"assets/src/ba_data/python/ba/_generated/__init__.py": "https://files.ballistica.net/cache/ba1/ee/e8/cad05aa531c7faf7ff7b96db7f6e",
"assets/src/ba_data/python/ba/_generated/enums.py": "https://files.ballistica.net/cache/ba1/b2/e5/0ee0561e16257a32830645239f34",
"ballisticacore-windows/Generic/BallisticaCore.ico": "https://files.ballistica.net/cache/ba1/89/c0/e32c7d2a35dc9aef57cc73b0911a",
"build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/e3/10/a6410ca04d9706646f77a1cd7432",
"build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/bc/98/4799abf4b7a56390650e0ca0d9bd",
"build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/bf/bf/1604a0293c5aa2e37911e1658771",
"build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/e3/00/fdddb7b085d10b9020b5e8a37737",
"build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/dd/9b/61f2a773d305b6603751192a9f6f",
"build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/cb/05/0e0d5b65658f56a1ce846ebf6223",
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/61/54/dd0fda3db51d62eafb1c98e36354",
"build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/07/31/fabf6c99c51347995a08934544dc",
"build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/dd/5b/c4e30243c2529ab39b6fb8350715",
"build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/c2/d6/781e395d0f179b6045d4d258a174",
"build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/59/52/9f6df32262d8326b4f037dce4292",
"build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/c0/74/4dcbb152ff5e166508d2fd8f6f33",
"build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/5f/e3/afcebfc9365c48aa5b8c655283cf",
"build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/ac/5c/c008db9992e94225a2ba7b19533f",
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/05/38/45d4fe4fbc9fe8c5a127c06a8503",
"build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ef/11/4fc7f1c786d282369326544cfbe0",
"build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/36/3e/5f3e0e88a2f645de91e6e748257a",
"build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/d5/78/d15d84ccbf8d44879b240aea0bd7",
"build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/bd/57/57a27ee6a9b9234861ef0ae5bdda",
"build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/04/80/c4abe4921c9dca83d764358d45f8",
"build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/cb/a6/560fb2889c78f14c47f7185b284c",
"build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/24/29/b23ac44bf2a76fcb5b4920d76bb2",
"build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/2e/1e/d4b6c04e0f7f277487bc21d88c44",
"build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/18/87/1a907cb4fbde86c26b295da615ef",
"build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a8/22/0215aaf316e7841a001840181f30",
"build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/5b/76/b68f63c60efb32d18bfda5ac2125",
"build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/19/01/ca0d7b90ca48daa28a8ff7b5f9e7",
"build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/c0/a7/d748772aa00b604fc43bb9ef0c21",
"build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/8b/e9/437a4918cf4d7eb6498f24a9ff16",
"build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ae/44/9bdd10f4bb0b2fdc67bc57624872",
"build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/12/5d/3a5acb1cbf7b390fe561aacecaaa",
"build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/16/ae/e32e6075da6ced98216aa83c87e3",
"build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/8c/38/d67105c92308fc168e2f82eb89db",
"build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/f5/9b/e02bc0842ec9422cdd1332928805",
"build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/91/8a/03c904a39698ba5804365bec8ae2",
"build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/44/a4/f76c3f28464408d10e0b4ba2b33c",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/46/ed/7a596e5d725752af99f08e39878b",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/c1/d7/b2368705dbbe68720c318778afc7",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/0f/4f/f12ef3415aa4339e650330cc310b",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/0f/67/5fea68f30efc01d82687be7fd452",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/da/c0/0707d6d205e9dfe4f613ba672918",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/a8/e1/726ea2e9710a12bb375bd5b8c43d",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/28/4f/38d2d59336874ec660cb909aad4b",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/57/9b/99e8f77f47dd8ce0272d59e990e0",
"build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/9a/74/282ba4731114c2d7caf25ecf7906",
"build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/4d/8a/99168a6e43fbe1396891de359d71",
"build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/f2/08/0662177c11669e6533cff41ca505",
"build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/b3/34/a5ec0c985c54fb9cd715008bc326",
"build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/e2/83/13a4b554ee947460454e81d70e3f",
"build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/38/b4/68bb2704cfa6a061341571ae00a8",
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/83/a5/8b267696cf929ad1e6d7e5e49f8b",
"build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/f2/12/205008f75fa65c0a715198e0942c",
"build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/28/2f/edcf3adbd3097299475e87e32084",
"build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/3e/f5/748d921986a48c7aaf3b14569751",
"build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/67/b5/1c678edd3720d83fa64c2ab0d83d",
"build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a9/d0/f81e3398dd298e2b674f2036a23e",
"build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/82/36/aeb2d725778a4ebaf7afd71f8750",
"build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/f2/92/e5d485610ee43060e7845d0e249e",
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/4b/07/e67992e54bee413fe030589b12c2",
"build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/c1/bf/966057408787088a3e821d4795b1",
"build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/fd/22/c43657b3bbe2f430c6de10aac8e6",
"build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/9b/55/06822d9cc2c656736d54817be996",
"build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/d1/26/06c368355613f179e9d6c52a1efb",
"build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/85/0e/3f3320a08003e40633ccf772da9d",
"build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b9/df/bf2e825628494d81e78d4c51e5ec",
"build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b4/8b/bcce0396452ed6fea409a6c3f1d2",
"build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/32/76/0c9686d9a11091fb436938d83a4c",
"build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/5c/83/55b33e7ccbb7e63807005f3e9da5",
"build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/c7/32/55b8a340b05da164d8d29f702915",
"build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/21/17/1a896a26cc9e4a5fb092a6419933",
"build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e8/9f/382298830c87ed692c41259da3c6",
"build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/21/69/67236ce45bcecc2a13e8d2ffa210",
"build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/39/85/2512bb6fdb5d7aefb3a2e54200f9",
"build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/7e/24/90c7ab793925086d951f16bc4b82",
"build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/8e/52/edf00d7fc606b5fd28c1360c9928",
"build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/82/57/54a1fc8de56c8f1fefe6947bc355",
"build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/4a/1e/8d6cd2d0d2bb4c19713f16b7b303",
"build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/9b/da/97ef92e8f6d7a834ab521eba3f1e",
"build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ab/81/3af07f1a43103d611774cc3600c9",
"build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d1/63/6a1b2d695a667df137094e13b3f8",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/63/3e/a571557b816d27f6483c6b576e9a",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/b5/fe/0270830ee87bab9e37defbe6e552",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/ee/db/dae2c2054701de01132a326299c6",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/14/bb/c35a583881212c553429f6957e0e",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/cf/d5/ace461574dad8a3ec7e06ad47cdf",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/2c/5c/bc8451a1c605130859cc08a2abe2",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/53/43/bec9c8419dbdc4caf76230d57569",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/cf/5f/a2be3b925b4b877aa86a5bd043e4",
"src/ballistica/generated/python_embedded/binding.inc": "https://files.ballistica.net/cache/ba1/6e/6f/004b696e9a13b083069374e4bb6a",
"src/ballistica/generated/python_embedded/bootstrap.inc": "https://files.ballistica.net/cache/ba1/d3/db/e73d4dcf1280d5f677c3cf8b47c3"
}

View File

@ -1,4 +1,4 @@
### 1.7.0 (20578, 2022-05-28)
### 1.7.0 (20580, 2022-05-30)
- V2 accounts are now available (woohoo!). These are called 'BombSquad Accounts' in the account section. V2 accounts communicate with a completely new server and will be the foundation for lots of new functionality in the future. However they also function as a V1 account so existing functionality should still work. Note that the new 'workspaces' feature for V2-accounts is not yet enabled in this build, but it will be in the next few builds. Also note that account types such as GameCenter and Google-Play will be 'upgraded' to V2 accounts in the future so there is no need to try this out if you use one of those. But if you use device-accounts you might want to create yourself a V2 account, since device-accounts will remain V1-only (though you can link an old device-account to a v2-enabled account if you want to keep your progress). Getting a V2 account now also gives you a chance to reserve a nice account-tag before all the good ones are taken.
- Legacy account subsystem has been renamed from `ba.app.accounts` to `ba.app.accounts_v1`
- Added `ba.app.accounts_v2` subsystem for working with V2 accounts.
@ -15,6 +15,7 @@
- `_ba.get_account_misc_read_val()` is now `_ba.get_v1_account_misc_read_val()`
- `_ba.get_account_misc_read_val_2()` is now `_ba.get_v1_account_misc_read_val_2()`
- `_ba.get_account_ticket_count()` is now `_ba.get_v1_account_ticket_count()`
- Exposing more sources in the public repo; namely networking stuff. I realize this probably opens up some attack vectors for hackers but also opens up options for server-owners to add their own defenses without having to wait on me. Hopefully this won't prove to be a bad idea.
### 1.6.12 (20567, 2022-05-04)

View File

@ -1 +1 @@
14398100813069830297938811166218395775
25124287646962522356366356681900113695

View File

@ -302,11 +302,17 @@ add_executable(ballisticacore
${BA_SRC_ROOT}/ballistica/dynamics/rigid_body.h
${BA_SRC_ROOT}/ballistica/game/account.h
${BA_SRC_ROOT}/ballistica/game/client_controller_interface.h
${BA_SRC_ROOT}/ballistica/game/connection/connection.cc
${BA_SRC_ROOT}/ballistica/game/connection/connection.h
${BA_SRC_ROOT}/ballistica/game/connection/connection_set.cc
${BA_SRC_ROOT}/ballistica/game/connection/connection_set.h
${BA_SRC_ROOT}/ballistica/game/connection/connection_to_client.cc
${BA_SRC_ROOT}/ballistica/game/connection/connection_to_client.h
${BA_SRC_ROOT}/ballistica/game/connection/connection_to_client_udp.cc
${BA_SRC_ROOT}/ballistica/game/connection/connection_to_client_udp.h
${BA_SRC_ROOT}/ballistica/game/connection/connection_to_host.cc
${BA_SRC_ROOT}/ballistica/game/connection/connection_to_host.h
${BA_SRC_ROOT}/ballistica/game/connection/connection_to_host_udp.cc
${BA_SRC_ROOT}/ballistica/game/connection/connection_to_host_udp.h
${BA_SRC_ROOT}/ballistica/game/friend_score_set.h
${BA_SRC_ROOT}/ballistica/game/game.cc
@ -488,8 +494,11 @@ add_executable(ballisticacore
${BA_SRC_ROOT}/ballistica/media/media.h
${BA_SRC_ROOT}/ballistica/media/media_server.cc
${BA_SRC_ROOT}/ballistica/media/media_server.h
${BA_SRC_ROOT}/ballistica/networking/network_reader.cc
${BA_SRC_ROOT}/ballistica/networking/network_reader.h
${BA_SRC_ROOT}/ballistica/networking/network_write_module.cc
${BA_SRC_ROOT}/ballistica/networking/network_write_module.h
${BA_SRC_ROOT}/ballistica/networking/networking.cc
${BA_SRC_ROOT}/ballistica/networking/networking.h
${BA_SRC_ROOT}/ballistica/networking/networking_sys.h
${BA_SRC_ROOT}/ballistica/networking/sockaddr.cc
@ -550,6 +559,8 @@ add_executable(ballisticacore
${BA_SRC_ROOT}/ballistica/python/methods/python_methods_input.h
${BA_SRC_ROOT}/ballistica/python/methods/python_methods_media.cc
${BA_SRC_ROOT}/ballistica/python/methods/python_methods_media.h
${BA_SRC_ROOT}/ballistica/python/methods/python_methods_networking.cc
${BA_SRC_ROOT}/ballistica/python/methods/python_methods_networking.h
${BA_SRC_ROOT}/ballistica/python/methods/python_methods_system.cc
${BA_SRC_ROOT}/ballistica/python/methods/python_methods_system.h
${BA_SRC_ROOT}/ballistica/python/methods/python_methods_ui.cc

View File

@ -293,11 +293,17 @@
<ClInclude Include="..\..\src\ballistica\dynamics\rigid_body.h" />
<ClInclude Include="..\..\src\ballistica\game\account.h" />
<ClInclude Include="..\..\src\ballistica\game\client_controller_interface.h" />
<ClCompile Include="..\..\src\ballistica\game\connection\connection.cc" />
<ClInclude Include="..\..\src\ballistica\game\connection\connection.h" />
<ClCompile Include="..\..\src\ballistica\game\connection\connection_set.cc" />
<ClInclude Include="..\..\src\ballistica\game\connection\connection_set.h" />
<ClCompile Include="..\..\src\ballistica\game\connection\connection_to_client.cc" />
<ClInclude Include="..\..\src\ballistica\game\connection\connection_to_client.h" />
<ClCompile Include="..\..\src\ballistica\game\connection\connection_to_client_udp.cc" />
<ClInclude Include="..\..\src\ballistica\game\connection\connection_to_client_udp.h" />
<ClCompile Include="..\..\src\ballistica\game\connection\connection_to_host.cc" />
<ClInclude Include="..\..\src\ballistica\game\connection\connection_to_host.h" />
<ClCompile Include="..\..\src\ballistica\game\connection\connection_to_host_udp.cc" />
<ClInclude Include="..\..\src\ballistica\game\connection\connection_to_host_udp.h" />
<ClInclude Include="..\..\src\ballistica\game\friend_score_set.h" />
<ClCompile Include="..\..\src\ballistica\game\game.cc" />
@ -479,8 +485,11 @@
<ClInclude Include="..\..\src\ballistica\media\media.h" />
<ClCompile Include="..\..\src\ballistica\media\media_server.cc" />
<ClInclude Include="..\..\src\ballistica\media\media_server.h" />
<ClCompile Include="..\..\src\ballistica\networking\network_reader.cc" />
<ClInclude Include="..\..\src\ballistica\networking\network_reader.h" />
<ClCompile Include="..\..\src\ballistica\networking\network_write_module.cc" />
<ClInclude Include="..\..\src\ballistica\networking\network_write_module.h" />
<ClCompile Include="..\..\src\ballistica\networking\networking.cc" />
<ClInclude Include="..\..\src\ballistica\networking\networking.h" />
<ClInclude Include="..\..\src\ballistica\networking\networking_sys.h" />
<ClCompile Include="..\..\src\ballistica\networking\sockaddr.cc" />
@ -541,6 +550,8 @@
<ClInclude Include="..\..\src\ballistica\python\methods\python_methods_input.h" />
<ClCompile Include="..\..\src\ballistica\python\methods\python_methods_media.cc" />
<ClInclude Include="..\..\src\ballistica\python\methods\python_methods_media.h" />
<ClCompile Include="..\..\src\ballistica\python\methods\python_methods_networking.cc" />
<ClInclude Include="..\..\src\ballistica\python\methods\python_methods_networking.h" />
<ClCompile Include="..\..\src\ballistica\python\methods\python_methods_system.cc" />
<ClInclude Include="..\..\src\ballistica\python\methods\python_methods_system.h" />
<ClCompile Include="..\..\src\ballistica\python\methods\python_methods_ui.cc" />

View File

@ -313,21 +313,39 @@
<ClInclude Include="..\..\src\ballistica\game\client_controller_interface.h">
<Filter>ballistica\game</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ballistica\game\connection\connection.cc">
<Filter>ballistica\game\connection</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ballistica\game\connection\connection.h">
<Filter>ballistica\game\connection</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ballistica\game\connection\connection_set.cc">
<Filter>ballistica\game\connection</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ballistica\game\connection\connection_set.h">
<Filter>ballistica\game\connection</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ballistica\game\connection\connection_to_client.cc">
<Filter>ballistica\game\connection</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ballistica\game\connection\connection_to_client.h">
<Filter>ballistica\game\connection</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ballistica\game\connection\connection_to_client_udp.cc">
<Filter>ballistica\game\connection</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ballistica\game\connection\connection_to_client_udp.h">
<Filter>ballistica\game\connection</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ballistica\game\connection\connection_to_host.cc">
<Filter>ballistica\game\connection</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ballistica\game\connection\connection_to_host.h">
<Filter>ballistica\game\connection</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ballistica\game\connection\connection_to_host_udp.cc">
<Filter>ballistica\game\connection</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ballistica\game\connection\connection_to_host_udp.h">
<Filter>ballistica\game\connection</Filter>
</ClInclude>
@ -871,12 +889,21 @@
<ClInclude Include="..\..\src\ballistica\media\media_server.h">
<Filter>ballistica\media</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ballistica\networking\network_reader.cc">
<Filter>ballistica\networking</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ballistica\networking\network_reader.h">
<Filter>ballistica\networking</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ballistica\networking\network_write_module.cc">
<Filter>ballistica\networking</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ballistica\networking\network_write_module.h">
<Filter>ballistica\networking</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ballistica\networking\networking.cc">
<Filter>ballistica\networking</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ballistica\networking\networking.h">
<Filter>ballistica\networking</Filter>
</ClInclude>
@ -1057,6 +1084,12 @@
<ClInclude Include="..\..\src\ballistica\python\methods\python_methods_media.h">
<Filter>ballistica\python\methods</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ballistica\python\methods\python_methods_networking.cc">
<Filter>ballistica\python\methods</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ballistica\python\methods\python_methods_networking.h">
<Filter>ballistica\python\methods</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ballistica\python\methods\python_methods_system.cc">
<Filter>ballistica\python\methods</Filter>
</ClCompile>

View File

@ -288,11 +288,17 @@
<ClInclude Include="..\..\src\ballistica\dynamics\rigid_body.h" />
<ClInclude Include="..\..\src\ballistica\game\account.h" />
<ClInclude Include="..\..\src\ballistica\game\client_controller_interface.h" />
<ClCompile Include="..\..\src\ballistica\game\connection\connection.cc" />
<ClInclude Include="..\..\src\ballistica\game\connection\connection.h" />
<ClCompile Include="..\..\src\ballistica\game\connection\connection_set.cc" />
<ClInclude Include="..\..\src\ballistica\game\connection\connection_set.h" />
<ClCompile Include="..\..\src\ballistica\game\connection\connection_to_client.cc" />
<ClInclude Include="..\..\src\ballistica\game\connection\connection_to_client.h" />
<ClCompile Include="..\..\src\ballistica\game\connection\connection_to_client_udp.cc" />
<ClInclude Include="..\..\src\ballistica\game\connection\connection_to_client_udp.h" />
<ClCompile Include="..\..\src\ballistica\game\connection\connection_to_host.cc" />
<ClInclude Include="..\..\src\ballistica\game\connection\connection_to_host.h" />
<ClCompile Include="..\..\src\ballistica\game\connection\connection_to_host_udp.cc" />
<ClInclude Include="..\..\src\ballistica\game\connection\connection_to_host_udp.h" />
<ClInclude Include="..\..\src\ballistica\game\friend_score_set.h" />
<ClCompile Include="..\..\src\ballistica\game\game.cc" />
@ -474,8 +480,11 @@
<ClInclude Include="..\..\src\ballistica\media\media.h" />
<ClCompile Include="..\..\src\ballistica\media\media_server.cc" />
<ClInclude Include="..\..\src\ballistica\media\media_server.h" />
<ClCompile Include="..\..\src\ballistica\networking\network_reader.cc" />
<ClInclude Include="..\..\src\ballistica\networking\network_reader.h" />
<ClCompile Include="..\..\src\ballistica\networking\network_write_module.cc" />
<ClInclude Include="..\..\src\ballistica\networking\network_write_module.h" />
<ClCompile Include="..\..\src\ballistica\networking\networking.cc" />
<ClInclude Include="..\..\src\ballistica\networking\networking.h" />
<ClInclude Include="..\..\src\ballistica\networking\networking_sys.h" />
<ClCompile Include="..\..\src\ballistica\networking\sockaddr.cc" />
@ -536,6 +545,8 @@
<ClInclude Include="..\..\src\ballistica\python\methods\python_methods_input.h" />
<ClCompile Include="..\..\src\ballistica\python\methods\python_methods_media.cc" />
<ClInclude Include="..\..\src\ballistica\python\methods\python_methods_media.h" />
<ClCompile Include="..\..\src\ballistica\python\methods\python_methods_networking.cc" />
<ClInclude Include="..\..\src\ballistica\python\methods\python_methods_networking.h" />
<ClCompile Include="..\..\src\ballistica\python\methods\python_methods_system.cc" />
<ClInclude Include="..\..\src\ballistica\python\methods\python_methods_system.h" />
<ClCompile Include="..\..\src\ballistica\python\methods\python_methods_ui.cc" />

View File

@ -313,21 +313,39 @@
<ClInclude Include="..\..\src\ballistica\game\client_controller_interface.h">
<Filter>ballistica\game</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ballistica\game\connection\connection.cc">
<Filter>ballistica\game\connection</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ballistica\game\connection\connection.h">
<Filter>ballistica\game\connection</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ballistica\game\connection\connection_set.cc">
<Filter>ballistica\game\connection</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ballistica\game\connection\connection_set.h">
<Filter>ballistica\game\connection</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ballistica\game\connection\connection_to_client.cc">
<Filter>ballistica\game\connection</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ballistica\game\connection\connection_to_client.h">
<Filter>ballistica\game\connection</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ballistica\game\connection\connection_to_client_udp.cc">
<Filter>ballistica\game\connection</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ballistica\game\connection\connection_to_client_udp.h">
<Filter>ballistica\game\connection</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ballistica\game\connection\connection_to_host.cc">
<Filter>ballistica\game\connection</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ballistica\game\connection\connection_to_host.h">
<Filter>ballistica\game\connection</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ballistica\game\connection\connection_to_host_udp.cc">
<Filter>ballistica\game\connection</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ballistica\game\connection\connection_to_host_udp.h">
<Filter>ballistica\game\connection</Filter>
</ClInclude>
@ -871,12 +889,21 @@
<ClInclude Include="..\..\src\ballistica\media\media_server.h">
<Filter>ballistica\media</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ballistica\networking\network_reader.cc">
<Filter>ballistica\networking</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ballistica\networking\network_reader.h">
<Filter>ballistica\networking</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ballistica\networking\network_write_module.cc">
<Filter>ballistica\networking</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ballistica\networking\network_write_module.h">
<Filter>ballistica\networking</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ballistica\networking\networking.cc">
<Filter>ballistica\networking</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ballistica\networking\networking.h">
<Filter>ballistica\networking</Filter>
</ClInclude>
@ -1057,6 +1084,12 @@
<ClInclude Include="..\..\src\ballistica\python\methods\python_methods_media.h">
<Filter>ballistica\python\methods</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ballistica\python\methods\python_methods_networking.cc">
<Filter>ballistica\python\methods</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ballistica\python\methods\python_methods_networking.h">
<Filter>ballistica\python\methods</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ballistica\python\methods\python_methods_system.cc">
<Filter>ballistica\python\methods</Filter>
</ClCompile>

View File

@ -21,7 +21,7 @@
namespace ballistica {
// These are set automatically via script; don't modify them here.
const int kAppBuildNumber = 20578;
const int kAppBuildNumber = 20580;
const char* kAppVersion = "1.7.0";
// Our standalone globals.

View File

@ -183,6 +183,13 @@ auto AppInternalOnGameThreadPause() -> void;
auto AppInternalDirectSendLogs(const std::string& prefix,
const std::string& suffix, bool instant,
int* result = nullptr) -> void;
auto AppInternalClientInfoQuery(const std::string& val1,
const std::string& val2,
const std::string& val3, int build_number)
-> void;
auto AppInternalCalcV1PeerHash(const std::string& peer_hash_input)
-> std::string;
auto AppInternalV1SetClientInfo(JsonDict* dict) -> void;
/// Does it appear that we are a blessed build with no known user-modifications?
auto IsUnmodifiedBlessedBuild() -> bool;

View File

@ -95,6 +95,7 @@ class Input;
class InputDevice;
struct JointFixedEF;
class Joystick;
class JsonDict;
class KeyboardInput;
class Material;
class MaterialAction;

View File

@ -0,0 +1,495 @@
// Released under the MIT License. See LICENSE for details.
#include "ballistica/game/connection/connection.h"
#include "ballistica/generic/huffman.h"
#include "ballistica/generic/json.h"
#include "ballistica/generic/utils.h"
#include "ballistica/math/vector3f.h"
#include "ballistica/networking/networking.h"
#include "ballistica/platform/platform.h"
namespace ballistica {
// How long to go without sending a state packet before
// we send keepalives. Keepalives contain the latest ack info.
const int kKeepaliveDelay = 100; // 1000/15
// How long before an individual packet is re-sent if we haven't gotten an ack.
const int kPacketResendTime = 100;
// How old a packet must be before we prune it.
const int kPacketPruneTime = 10000;
// How long to go between pruning our packets.
const int kPacketPruneInterval = 1000;
Connection::Connection() {
// NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer)
creation_time_ = last_average_update_time_ = GetRealTime();
}
void Connection::ProcessWaitingMessages() {
// Process waiting in-messages until we find one that's missing.
while (true) {
auto i = in_messages_.find(next_in_message_num_);
if (i == in_messages_.end()) {
break;
}
HandleMessagePacket(i->second.data);
in_messages_.erase(i);
next_in_message_num_++;
// Moving to a new in-message-num also resets our next-unreliable-num.
next_in_unreliable_message_num_ = 0;
}
}
void Connection::EmbedAcks(millisecs_t real_time, std::vector<uint8_t>* data,
int offset) {
assert(data);
// Store full value for the next message num we want.
memcpy(data->data() + offset, &next_in_message_num_,
sizeof(next_in_message_num_));
// Now store a 1-byte bitfield telling which of the 8 messages following
// next_in_message_num_ we already have. This helps prevent redundant
// re-sends on the other end if we just missed one random packet, etc.
uint8_t extra_bits = 0;
uint16_t num = next_in_message_num_;
for (uint32_t i = 0; i < 8; i++) {
if (in_messages_.find(++num) != in_messages_.end()) {
extra_bits |= (0x01u << i);
}
}
(*data)[offset + 2] = extra_bits;
last_ack_send_time_ = real_time;
}
void Connection::HandleResends(millisecs_t real_time,
const std::vector<uint8_t>& data, int offset) {
// Pull the next number they want.
uint16_t their_next_in;
memcpy(&their_next_in, data.data() + offset, sizeof(their_next_in));
// Along with a bit-field of which ones after that they already have..
// (prevents some un-necessary re-sending)
uint8_t extra_bits = data[offset + 2];
// Get a rough ping by looking at the previous packet and measuring its
// round-trip time if it has not yet been ack'ed.
auto test_num = static_cast<uint16_t>(their_next_in - 1u);
auto j = out_messages_.find(test_num);
if (j != out_messages_.end()) {
ReliableMessageOut& msg(j->second);
if (!msg.acked) {
float smoothing = 0.95f;
average_ping_ =
smoothing * average_ping_
+ (1.0f - smoothing)
* static_cast<float>(real_time - msg.first_send_time);
}
msg.acked = true;
}
// Re-send up to 9 un-acked packets if it's been long enough.
// (their next requested plus their 8 extra-bits)
uint16_t num = their_next_in;
for (uint32_t i = 0; i < 9; i++) {
// If we've reached our next out-number, we havn't sent it yet so we're
// peachy.
if (num == next_out_message_num_) break;
bool they_want_this_packet;
if (i == 0) {
// They *always* want the one they're asking for.
they_want_this_packet = true;
} else {
they_want_this_packet = ((extra_bits & (0x01u << (i - 1))) == 0);
}
// If we have no record for this out-packet, it's too old; abort the
// connection.
auto j2 = out_messages_.find(num);
if (j2 == out_messages_.end()) {
Error("");
return;
}
ReliableMessageOut& msg(j2->second);
// Check with the actual packet for ack state (it may have been acked by
// another packet but not this one).
if (!they_want_this_packet) {
msg.acked = true;
}
// If its un-acked and older than our threshold, re-send.
if (!msg.acked && real_time - msg.last_send_time > msg.resend_time) {
msg.resend_time *= 2; // wait twice as long with each resend..
msg.last_send_time = real_time;
// Add our header/acks and go ahead and send this one out.
// 1 byte for type, 2 for packet-num, 3 for acks
std::vector<uint8_t> data_out(msg.data.size() + kMessagePacketHeaderSize);
data_out[0] = BA_GAMEPACKET_MESSAGE;
memcpy(data_out.data() + 1, &num, sizeof(num));
EmbedAcks(real_time, &data_out, 3);
memcpy(&(data_out[6]), &(msg.data[0]), msg.data.size());
SendGamePacket(data_out);
resend_packet_count_++;
resend_bytes_out_ += data_out.size();
}
num++;
}
}
void Connection::HandleGamePacketCompressed(const std::vector<uint8_t>& data) {
std::vector<uint8_t> data_decompressed;
try {
data_decompressed = g_utils->huffman()->decompress(data);
} catch (const std::exception& e) {
Log(std::string("EXC in huffman decompression for packet: ") + e.what());
// Hmmm i guess lets just ignore this packet and keep on trucking?.. or
// should we kill the connection?
return;
}
bytes_in_compressed_ += data.size();
HandleGamePacket(data_decompressed);
packet_count_in_++;
bytes_in_ += data_decompressed.size();
}
void Connection::HandleGamePacket(const std::vector<uint8_t>& data) {
// Sub-classes shouldn't let invalid messages get to us.
assert(!data.empty());
switch (data[0]) {
case BA_GAMEPACKET_KEEPALIVE: {
if (data.size() != 4) {
BA_LOG_ONCE("Error: got invalid BA_GAMEPACKET_KEEPALIVE packet.");
return;
}
millisecs_t real_time = GetRealTime();
HandleResends(real_time, data, 1);
break;
}
case BA_GAMEPACKET_MESSAGE: {
millisecs_t real_time = GetRealTime();
// Expect 1 byte type, 2 byte num, 3 byte acks, at least 1 byte payload.
if (data.size() < 7) {
Log("Error: Got invalid BA_PACKET_STATE packet.");
return;
}
uint16_t num;
memcpy(&num, data.data() + 1, sizeof(num));
// Run any necessary re-sends based on this guy's acks.
HandleResends(real_time, data, 3);
// If they're an upcoming message number this difference will be small;
// otherwise we can ignore them since they're in the past.
if (num - next_in_message_num_ > 32000) {
return;
}
// Store this packet.
ReliableMessageIn& msg(in_messages_[num]);
msg.data.resize(data.size() - 6);
memcpy(&(msg.data[0]), &(data[6]), msg.data.size());
msg.arrival_time = GetRealTime();
// Now run all in-order packets we've got.
ProcessWaitingMessages();
break;
}
case BA_GAMEPACKET_MESSAGE_UNRELIABLE: {
// Expect 1 byte type, 2 byte num, 2 byte unreliable-num, 3 byte acks,
// at least 1 byte payload.
if (data.size() < 9) {
Log("Error: Got invalid BA_PACKET_STATE_UNRELIABLE packet.");
return;
}
uint16_t num, num_unreliable;
memcpy(&num, data.data() + 1, sizeof(num));
memcpy(&num_unreliable, data.data() + 3, sizeof(num_unreliable));
// *ONLY* apply this if its num is the next one we're waiting for and
// num_unreliable is >= our next unreliable num
if (num == next_in_message_num_
&& num_unreliable >= next_in_unreliable_message_num_) {
std::vector<uint8_t> msg_data(data.size() - 8);
memcpy(&(msg_data[0]), &(data[8]), msg_data.size());
HandleMessagePacket(msg_data);
next_in_unreliable_message_num_ =
static_cast<uint16_t>(num_unreliable + 1u);
}
break;
}
default:
Log("Connection got unknown packet type: "
+ std::to_string(static_cast<int>(data[0])));
break;
}
}
void Connection::Error(const std::string& msg) {
// If we've already errored, just ignore.
if (errored_) {
return;
}
errored_ = true;
if (!msg.empty()) {
ScreenMessage(msg, {1.0f, 0.0, 0.0f});
}
}
void Connection::SendReliableMessage(const std::vector<uint8_t>& data) {
assert(!data.empty());
// If our connection is going down, silently ignore this.
if (connection_dying_) {
return;
}
// To allow sending messages of any size, we transparently break large
// messages up into BA_MESSAGE_MULTIPART messages which are transparently
// re-assembled on the other end.
if (data.size() > 480) {
auto data_size = static_cast<uint32_t>(data.size());
uint32_t part_start = 0;
uint32_t part_size = 479;
while (true) {
// If this takes us to the end of the message, send a multipart-end.
if ((part_start + part_size) >= data_size) {
part_size = data_size - part_start;
assert(part_size > 0);
// 1 byte type plus data
std::vector<uint8_t> part_message(1 + part_size);
part_message[0] = BA_MESSAGE_MULTIPART_END;
memcpy(&(part_message[1]), &(data[part_start]), part_size);
SendReliableMessage(part_message);
return;
} else {
std::vector<uint8_t> part_message(1 + part_size);
part_message[0] = BA_MESSAGE_MULTIPART;
memcpy(&(part_message[1]), &(data[part_start]), part_size);
SendReliableMessage(part_message);
}
part_start += part_size;
}
}
uint16_t num = next_out_message_num_++;
// By incrementing reliable-message-num we reset the unreliable num.
next_out_unreliable_message_num_ = 0;
// Add an entry for it.
assert(out_messages_.find(num) == out_messages_.end());
ReliableMessageOut& msg(out_messages_[num]);
millisecs_t real_time = GetRealTime();
msg.data = data;
msg.first_send_time = msg.last_send_time = real_time;
msg.resend_time = kPacketResendTime;
msg.acked = false;
// Add our header/acks and go ahead and send this one out.
// 1 byte for type, 2 for packet-num, 3 for acks
std::vector<uint8_t> data_out(data.size() + kMessagePacketHeaderSize);
data_out[0] = BA_GAMEPACKET_MESSAGE;
memcpy(data_out.data() + 1, &num, sizeof(num));
EmbedAcks(real_time, &data_out, 3);
memcpy(&(data_out[6]), &(data[0]), data.size());
SendGamePacket(data_out);
}
void Connection::SendUnreliableMessage(const std::vector<uint8_t>& data) {
// For now we just silently drop anything bigger than our max packet size.
if (data.size() + 8 > kMaxPacketSize) {
BA_LOG_ONCE("Error: Dropping outgoing unreliable packet of size "
+ std::to_string(data.size()) + ".");
return;
}
// If our connection is going down, silently ignore this.
if (connection_dying_) {
return;
}
uint16_t num = next_out_unreliable_message_num_++;
millisecs_t real_time = GetRealTime();
// Add our header/acks and go ahead and send this one out.
// 1 byte for type, 2 for packet-num, 2 for unreliable packet-num, 3 for acks.
std::vector<uint8_t> data_out(data.size() + 8);
data_out[0] = BA_GAMEPACKET_MESSAGE_UNRELIABLE;
memcpy(data_out.data() + 1, &next_out_message_num_,
sizeof(next_out_message_num_));
memcpy(data_out.data() + 3, &num, sizeof(num));
EmbedAcks(real_time, &data_out, 5);
memcpy(&(data_out[8]), &(data[0]), data.size());
SendGamePacket(data_out);
}
void Connection::SendJMessage(cJSON* val) {
char* s = cJSON_PrintUnformatted(val);
auto s_len = static_cast<size_t>(strlen(s));
std::vector<uint8_t> msg(1u + s_len + 1u);
msg[0] = BA_MESSAGE_JMESSAGE;
memcpy(msg.data() + 1u, s, s_len + 1u);
free(s);
SendReliableMessage(msg);
}
void Connection::Update() {
millisecs_t real_time = GetRealTime();
// Update our averages once per second.
while (real_time - last_average_update_time_ > 1000) {
last_average_update_time_ += 1000; // Don't want this to drift.
last_resend_packet_count_ = resend_packet_count_;
last_resend_bytes_out_ = resend_bytes_out_;
last_bytes_out_ = bytes_out_;
last_bytes_out_compressed_ = bytes_out_compressed_;
last_packet_count_out_ = packet_count_out_;
last_bytes_in_ = bytes_in_;
last_bytes_in_compressed_ = bytes_in_compressed_;
last_packet_count_in_ = packet_count_in_;
bytes_out_ = packet_count_out_ = bytes_out_compressed_ = 0;
bytes_in_ = bytes_in_compressed_ = packet_count_in_ = 0;
resend_packet_count_ = resend_bytes_out_ = 0;
}
if (can_communicate() && real_time - last_ack_send_time_ > kKeepaliveDelay) {
// If we haven't sent anything with an ack out in a while, send along
// a keepalive packet (a packet containing nothing but an ack).
// 1 byte type, 2 byte next-expected, 1 byte extra-acks.
std::vector<uint8_t> data(4);
data[0] = BA_GAMEPACKET_KEEPALIVE;
EmbedAcks(real_time, &data, 1);
SendGamePacket(data);
}
// Occasionally prune our in and out messages.
if (real_time - last_prune_time_ > kPacketPruneInterval) {
last_prune_time_ = real_time;
{
int prune_count = 0;
for (auto i = out_messages_.begin(); i != out_messages_.end();) {
if (real_time - i->second.first_send_time > kPacketPruneTime) {
auto i_next = i;
i_next++;
out_messages_.erase(i);
prune_count++;
i = i_next;
} else {
i++;
}
}
}
{
int prune_count = 0;
for (auto i = in_messages_.begin(); i != in_messages_.end();) {
if (real_time - i->second.arrival_time > kPacketPruneTime) {
auto i_next = i;
i_next++;
in_messages_.erase(i);
prune_count++;
i = i_next;
} else {
i++;
}
}
}
}
}
void Connection::HandleMessagePacket(const std::vector<uint8_t>& buffer) {
switch (buffer[0]) {
// Re-assemble multipart messages that come in and pass them along as
// regular messages.
case BA_MESSAGE_MULTIPART:
case BA_MESSAGE_MULTIPART_END: {
if (buffer.size() > 1) {
// Append everything minus the type byte.
auto old_size = static_cast<uint32_t>(multipart_buffer_.size());
multipart_buffer_.resize(old_size + (buffer.size() - 1));
memcpy(&(multipart_buffer_[old_size]), &(buffer[1]), buffer.size() - 1);
} else {
Log("got invalid BA_MESSAGE_MULTIPART");
}
if (buffer[0] == BA_MESSAGE_MULTIPART_END) {
HandleMessagePacket(multipart_buffer_);
multipart_buffer_.clear();
}
break;
}
default: {
// Let's silently ignore these since we may be adding various
// messages mid-protocol in a backwards-compatible way.
// BA_LOG_ONCE("Got unrecognized packet type:
// "+std::to_string(int(buffer[0])));
}
}
}
void Connection::SendGamePacket(const std::vector<uint8_t>& data) {
// Don't want to call a pure-virtual SendGamePacketCompressed().
if (connection_dying_) {
return;
}
assert(!data.empty());
// Normally we withhold all packets until we know we speak the
// same language. However, DISCONNECT is a special case.
// (if we don't speak the same language we still need to be
// able to tell them to buzz off)
bool can_send = can_communicate();
if (data[0] == BA_GAMEPACKET_DISCONNECT) {
can_send = true;
}
// We aren't allowed to send anything out except handshakes until
// we've established that we can speak their language.
// If something does come through, just ignore it.
if (!can_send && data[0] != BA_GAMEPACKET_HANDSHAKE
&& data[0] != BA_GAMEPACKET_HANDSHAKE_RESPONSE) {
if (explicit_bool(false)) {
BA_LOG_ONCE("SendGamePacket() called before can_communicate set ("
+ g_platform->DemangleCXXSymbol(typeid(*this).name())
+ " ptype " + std::to_string(static_cast<int>(data[0]))
+ ")");
}
return;
}
packet_count_out_++;
bytes_out_ += data.size();
// We huffman-compress gamepackets on their way out.
std::vector<uint8_t> data_compressed = g_utils->huffman()->compress(data);
#if kTestPacketDrops
if (rand() % 100 < kTestPacketDropPercent) { // NOLINT
return;
}
#endif
bytes_out_compressed_ += data_compressed.size();
SendGamePacketCompressed(data_compressed);
}
} // namespace ballistica

View File

@ -0,0 +1,707 @@
// Released under the MIT License. See LICENSE for details.
#include "ballistica/game/connection/connection_set.h"
#include "ballistica/game/connection/connection_to_client_udp.h"
#include "ballistica/game/connection/connection_to_host_udp.h"
#include "ballistica/game/game.h"
#include "ballistica/game/player.h"
#include "ballistica/game/session/host_session.h"
#include "ballistica/input/device/input_device.h"
#include "ballistica/networking/network_write_module.h"
#include "ballistica/networking/sockaddr.h"
#include "ballistica/python/python.h"
#include "ballistica/python/python_sys.h"
namespace ballistica {
ConnectionSet::ConnectionSet() = default;
auto ConnectionSet::GetConnectionToHostUDP() -> ConnectionToHostUDP* {
ConnectionToHost* h = connection_to_host_.get();
return h ? h->GetAsUDP() : nullptr;
}
void ConnectionSet::RegisterClientController(ClientControllerInterface* c) {
// This shouldn't happen, but if there's already a controller registered,
// detach all clients from it.
if (client_controller_) {
Log("RegisterClientController() called "
"but already have a controller; bad.");
for (auto&& i : connections_to_clients_) {
assert(i.second.exists());
i.second->SetController(nullptr);
}
}
// Ok, now assign the new and attach all currently-connected clients to it.
client_controller_ = c;
if (client_controller_) {
for (auto&& i : connections_to_clients_) {
assert(i.second.exists());
if (i.second->can_communicate()) {
i.second->SetController(client_controller_);
}
}
}
}
auto ConnectionSet::Update() -> void {
// First do housekeeping on our client/host connections.
for (auto&& i : connections_to_clients_) {
BA_IFDEBUG(Object::WeakRef<ConnectionToClient> test_ref(i.second));
i.second->Update();
// Make sure the connection didn't kill itself in the update.
assert(test_ref.exists());
}
if (connection_to_host_.exists()) {
connection_to_host_->Update();
}
}
auto ConnectionSet::GetConnectedClientCount() const -> int {
assert(InGameThread());
int count = 0;
for (auto&& i : connections_to_clients_) {
if (i.second.exists() && i.second->can_communicate()) {
count++;
}
}
return count;
}
void ConnectionSet::SendChatMessage(const std::string& message,
const std::vector<int>* clients,
const std::string* sender_override) {
// Sending to particular clients is only applicable while hosting.
if (clients != nullptr && connection_to_host() != nullptr) {
throw Exception("Can't send chat message to specific clients as a client.");
}
// Same with overriding sender name
if (sender_override != nullptr && connection_to_host() != nullptr) {
throw Exception(
"Can't send chat message with sender_override as a client.");
}
std::string our_spec_string;
if (sender_override != nullptr) {
std::string override_final = *sender_override;
if (override_final.size() > kMaxPartyNameCombinedSize) {
override_final.resize(kMaxPartyNameCombinedSize);
override_final += "...";
}
our_spec_string =
PlayerSpec::GetDummyPlayerSpec(override_final).GetSpecString();
} else {
if (connection_to_host() != nullptr) {
// NOTE - we send our own spec string with the chat message whether we're
// a client or server.. however on protocol version 30+ this is ignored
// by the server and replaced with a spec string it generates for us.
// so once we know we're connected to a 30+ server we can start sending
// blank strings as a client.
// (not that it really matters; chat messages are tiny overall)
our_spec_string = PlayerSpec::GetAccountPlayerSpec().GetSpecString();
} else {
// As a host we want to do the equivalent of
// ConnectionToClient::GetCombinedSpec() except for local connections (so
// send our name as the combination of local players if possible). Look
// for players coming from this client-connection; if we find any, make a
// spec out of their name(s).
std::string p_name_combined;
if (auto* hs =
dynamic_cast<HostSession*>(g_game->GetForegroundSession())) {
for (auto&& p : hs->players()) {
InputDevice* input_device = p->GetInputDevice();
if (p->accepted() && p->name_is_real() && input_device != nullptr
&& !input_device->IsRemoteClient()) {
if (!p_name_combined.empty()) {
p_name_combined += "/";
}
p_name_combined += p->GetName();
}
}
}
if (p_name_combined.size() > kMaxPartyNameCombinedSize) {
p_name_combined.resize(kMaxPartyNameCombinedSize);
p_name_combined += "...";
}
if (!p_name_combined.empty()) {
our_spec_string =
PlayerSpec::GetDummyPlayerSpec(p_name_combined).GetSpecString();
} else {
our_spec_string = PlayerSpec::GetAccountPlayerSpec().GetSpecString();
}
}
}
// If we find a newline, only take the first line (prevent people from
// covering the screen with obnoxious chat messages).
std::string message2 = message;
size_t nlpos = message2.find('\n');
if (nlpos != std::string::npos) {
message2 = message2.substr(0, nlpos);
}
// If we're the host, run filters before we send the message out.
// If the filter kills the message, don't send.
bool allow_message = g_python->FilterChatMessage(&message2, -1);
if (!allow_message) {
return;
}
// 1 byte type + 1 byte spec-string-length + message.
std::vector<uint8_t> msg_out(1 + 1 + our_spec_string.size()
+ message2.size());
msg_out[0] = BA_MESSAGE_CHAT;
size_t spec_size = our_spec_string.size();
assert(spec_size < 256);
msg_out[1] = static_cast<uint8_t>(spec_size);
memcpy(&(msg_out[2]), our_spec_string.c_str(), spec_size);
memcpy(&(msg_out[2 + spec_size]), message2.c_str(), message2.size());
// If we're a client, send this to the host (it will make its way back to us
// when they send to clients).
if (ConnectionToHost* hc = connection_to_host()) {
hc->SendReliableMessage(msg_out);
} else {
// Ok we're the host.
// Send to all (or at least some) connected clients.
for (auto&& i : connections_to_clients_) {
// Skip if its going to specific ones and this one doesn't match.
if (clients != nullptr) {
auto found = false;
for (auto&& c : *clients) {
if (c == i.second->id()) {
found = true;
}
}
if (!found) {
continue;
}
}
if (i.second->can_communicate()) {
i.second->SendReliableMessage(msg_out);
}
}
// And display locally if the message is addressed to all.
if (clients == nullptr) {
g_game->LocalDisplayChatMessage(msg_out);
}
}
}
// Can probably kill this.
auto ConnectionSet::GetConnectionsToClients()
-> std::vector<ConnectionToClient*> {
std::vector<ConnectionToClient*> connections;
connections.reserve(connections_to_clients_.size());
for (auto& connections_to_client : connections_to_clients_) {
if (connections_to_client.second.exists()) {
connections.push_back(connections_to_client.second.get());
} else {
Log("HAVE NONEXISTENT CONNECTION_TO_CLIENT IN LIST; UNEXPECTED");
}
}
return connections;
}
void ConnectionSet::PushUDPConnectionPacketCall(
const std::vector<uint8_t>& data, const SockAddr& addr) {
// Avoid buffer-full errors if something is causing us to write too often;
// these are unreliable messages so its ok to just drop them.
if (!g_game->CheckPushSafety()) {
BA_LOG_ONCE(
"Ignoring excessive udp-connection input packets; (could this be a "
"flood attack?).");
return;
}
g_game->PushCall([this, data, addr] { UDPConnectionPacket(data, addr); });
}
auto ConnectionSet::Shutdown() -> void {
// If we have any client/host connections, give them
// a chance to shoot off disconnect packets or whatnot.
for (auto& connection : connections_to_clients_) {
connection.second->RequestDisconnect();
}
if (connection_to_host_.exists()) {
connection_to_host_->RequestDisconnect();
}
}
void ConnectionSet::SendScreenMessageToClients(const std::string& s, float r,
float g, float b) {
for (auto&& i : connections_to_clients_) {
if (i.second.exists() && i.second->can_communicate()) {
i.second->SendScreenMessage(s, r, g, b);
}
}
}
void ConnectionSet::SendScreenMessageToSpecificClients(
const std::string& s, float r, float g, float b,
const std::vector<int>& clients) {
for (auto&& i : connections_to_clients_) {
if (i.second.exists() && i.second->can_communicate()) {
// Only send if this client is in our list.
for (auto c : clients) {
if (c == i.second->id()) {
i.second->SendScreenMessage(s, r, g, b);
break;
}
}
}
}
// Now print locally only if -1 is in our list.
for (auto c : clients) {
if (c == -1) {
ScreenMessage(s, {r, g, b});
break;
}
}
}
void ConnectionSet::SendScreenMessageToAll(const std::string& s, float r,
float g, float b) {
SendScreenMessageToClients(s, r, g, b);
ScreenMessage(s, {r, g, b});
}
auto ConnectionSet::PrepareForLaunchHostSession() -> void {
// If for some reason we're still attached to a host, kill the connection.
if (connection_to_host_.exists()) {
Log("Had host-connection during LaunchHostSession(); shouldn't happen.");
connection_to_host_->RequestDisconnect();
connection_to_host_.Clear();
has_connection_to_host_ = false;
g_game->UpdateGameRoster();
}
}
auto ConnectionSet::HandleClientDisconnected(int id) -> void {
auto i = connections_to_clients_.find(id);
if (i != connections_to_clients_.end()) {
bool was_connected = i->second->can_communicate();
std::string leaver_spec = i->second->peer_spec().GetSpecString();
std::vector<uint8_t> leave_msg(leaver_spec.size() + 1);
leave_msg[0] = BA_MESSAGE_PARTY_MEMBER_LEFT;
memcpy(&(leave_msg[1]), leaver_spec.c_str(), leaver_spec.size());
connections_to_clients_.erase(i);
// If the client was connected, they were on the roster.
// We need to update it and send it to all remaining clients since they're
// gone. Also inform everyone who just left so they can announce it
// (technically could consolidate these messages but whatever...).
if (was_connected) {
g_game->UpdateGameRoster();
for (auto&& connection : connections_to_clients_) {
if (g_game->ShouldAnnouncePartyJoinsAndLeaves()) {
connection.second->SendReliableMessage(leave_msg);
}
}
}
}
}
auto ConnectionSet::DisconnectClient(int client_id, int ban_seconds) -> bool {
assert(InGameThread());
if (connection_to_host_.exists()) {
// Kick-votes first appeared in 14248
if (connection_to_host_->build_number() < 14248) {
return false;
}
if (client_id > 255) {
Log("DisconnectClient got client_id > 255 (" + std::to_string(client_id)
+ ")");
} else {
std::vector<uint8_t> msg_out(2);
msg_out[0] = BA_MESSAGE_KICK_VOTE;
msg_out[1] = static_cast_check_fit<uint8_t>(client_id);
connection_to_host_->SendReliableMessage(msg_out);
return true;
}
} else {
// No host connection - look for clients.
auto i = connections_to_clients_.find(client_id);
if (i != connections_to_clients_.end()) {
// If this is considered a kick, add an entry to our banned list so we
// know not to let them back in for a while.
if (ban_seconds > 0) {
g_game->BanPlayer(i->second->peer_spec(), 1000 * ban_seconds);
}
i->second->RequestDisconnect();
// Do the official local disconnect immediately with the sounds and all
// that.
PushClientDisconnectedCall(client_id);
return true;
}
}
return false;
}
void ConnectionSet::PushClientDisconnectedCall(int id) {
g_game->PushCall([this, id] { HandleClientDisconnected(id); });
}
void ConnectionSet::PushDisconnectedFromHostCall() {
g_game->PushCall([this] {
if (connection_to_host_.exists()) {
bool was_connected = connection_to_host_->can_communicate();
connection_to_host_.Clear();
has_connection_to_host_ = false;
// Clear out our party roster.
g_game->UpdateGameRoster();
// Go back to main menu *if* the connection was fully connected.
// Otherwise we're still probably sitting at the main menu
// so no need to reset it.
if (was_connected) {
g_game->RunMainMenu();
}
}
});
}
void ConnectionSet::PushHostConnectedUDPCall(const SockAddr& addr,
bool print_connect_progress) {
g_game->PushCall([this, addr, print_connect_progress] {
// Attempt to disconnect any clients we have, turn off public-party
// advertising, etc.
g_game->CleanUpBeforeConnectingToHost();
print_udp_connect_progress_ = print_connect_progress;
connection_to_host_ = Object::New<ConnectionToHostUDP>(addr);
has_connection_to_host_ = true;
printed_host_disconnect_ = false;
});
}
void ConnectionSet::PushDisconnectFromHostCall() {
g_game->PushCall([this] {
if (connection_to_host_.exists()) {
connection_to_host_->RequestDisconnect();
}
});
}
auto ConnectionSet::UnregisterClientController(ClientControllerInterface* c)
-> void {
assert(c);
// This shouldn't happen.
if (client_controller_ != c) {
Log("UnregisterClientController() called with a non-registered "
"controller");
return;
}
// Ok, detach all our controllers from this guy.
if (client_controller_) {
for (auto&& i : connections_to_clients_) {
i.second->SetController(nullptr);
}
}
client_controller_ = nullptr;
}
void ConnectionSet::ForceDisconnectClients() {
for (auto&& i : connections_to_clients_) {
if (ConnectionToClient* client = i.second.get()) {
client->RequestDisconnect();
}
}
connections_to_clients_.clear();
}
// Called for low level packets coming in pertaining to udp
// host/client-connections.
auto ConnectionSet::UDPConnectionPacket(const std::vector<uint8_t>& data_in,
const SockAddr& addr) -> void {
assert(!data_in.empty());
const uint8_t* data = &(data_in[0]);
auto data_size = static_cast<size_t>(data_in.size());
switch (data[0]) {
case BA_PACKET_CLIENT_ACCEPT: {
if (data_size == 3) {
uint8_t request_id = data[2];
// If we have a udp-host-connection and its request-id matches, we're
// accepted; hooray!
ConnectionToHostUDP* hc = GetConnectionToHostUDP();
if (hc && hc->request_id() == request_id) {
hc->set_client_id(data[1]);
}
}
break;
}
case BA_PACKET_DISCONNECT_FROM_CLIENT_REQUEST: {
if (data_size == 2) {
// Client is telling us (host) that it wants to disconnect.
uint8_t client_id = data[1];
// Wipe that client out (if it still exists).
PushClientDisconnectedCall(client_id);
// Now send an ack so they know it's been taken care of.
g_network_write_module->PushSendToCall(
{BA_PACKET_DISCONNECT_FROM_CLIENT_ACK, client_id}, addr);
}
break;
}
case BA_PACKET_DISCONNECT_FROM_CLIENT_ACK: {
if (data_size == 2) {
// Host is telling us (client) that we've been disconnected.
uint8_t client_id = data[1];
ConnectionToHostUDP* hc = GetConnectionToHostUDP();
if (hc && hc->client_id() == client_id) {
PushDisconnectedFromHostCall();
}
}
break;
}
case BA_PACKET_DISCONNECT_FROM_HOST_REQUEST: {
if (data_size == 2) {
uint8_t client_id = data[1];
// Host is telling us (client) to disconnect.
ConnectionToHostUDP* hc = GetConnectionToHostUDP();
if (hc && hc->client_id() == client_id) {
PushDisconnectedFromHostCall();
}
// Now send an ack so they know it's been taken care of.
g_network_write_module->PushSendToCall(
{BA_PACKET_DISCONNECT_FROM_HOST_ACK, client_id}, addr);
}
break;
}
case BA_PACKET_DISCONNECT_FROM_HOST_ACK: {
break;
}
case BA_PACKET_CLIENT_GAMEPACKET_COMPRESSED: {
if (data_size > 2) {
uint8_t client_id = data[1];
auto i = connections_to_clients_.find(client_id);
if (i != connections_to_clients_.end()) {
// FIXME: could change HandleGamePacketCompressed to avoid this
// copy.
std::vector<uint8_t> data2(data_size - 2);
memcpy(data2.data(), data + 2, data_size - 2);
i->second->HandleGamePacketCompressed(data2);
return;
} else {
// Send a disconnect request aimed at them.
g_network_write_module->PushSendToCall(
{BA_PACKET_DISCONNECT_FROM_HOST_REQUEST, client_id}, addr);
}
}
break;
}
case BA_PACKET_HOST_GAMEPACKET_COMPRESSED: {
if (data_size > 2) {
uint8_t request_id = data[1];
ConnectionToHostUDP* hc = GetConnectionToHostUDP();
if (hc && hc->request_id() == request_id) {
// FIXME: Should change HandleGamePacketCompressed to avoid this copy.
std::vector<uint8_t> data2(data_size - 2);
memcpy(data2.data(), data + 2, data_size - 2);
hc->HandleGamePacketCompressed(data2);
}
}
break;
}
case BA_PACKET_CLIENT_DENY:
case BA_PACKET_CLIENT_DENY_PARTY_FULL:
case BA_PACKET_CLIENT_DENY_ALREADY_IN_PARTY:
case BA_PACKET_CLIENT_DENY_VERSION_MISMATCH: {
if (data_size == 2) {
uint8_t request_id = data[1];
ConnectionToHostUDP* hc = GetConnectionToHostUDP();
// If they're for-sure rejecting *this* connection, kill it.
if (hc && hc->request_id() == request_id) {
bool keep_trying = false;
// OBSOLETE BUT HERE FOR BACKWARDS COMPAT WITH 1.4.98 servers.
// Newer servers never deny us in this way and simply include
// their protocol version in the handshake they send us, allowing us
// to decide whether we support talking to them or not.
if (data[0] == BA_PACKET_CLIENT_DENY_VERSION_MISMATCH) {
// If we've got more protocols we can try, keep trying to connect
// with our other protocols until one works or we run out.
// FIXME: We should move this logic to the gamepacket or message
// level so it works for all connection types.
keep_trying = hc->SwitchProtocol();
if (!keep_trying) {
if (!printed_host_disconnect_) {
ScreenMessage(g_game->GetResourceString(
"connectionFailedVersionMismatchText"),
{1, 0, 0});
printed_host_disconnect_ = true;
}
}
} else if (data[0] == BA_PACKET_CLIENT_DENY_PARTY_FULL) {
if (!printed_host_disconnect_) {
if (print_udp_connect_progress_) {
ScreenMessage(
g_game->GetResourceString("connectionFailedPartyFullText"),
{1, 0, 0});
}
printed_host_disconnect_ = true;
}
} else if (data[0] == BA_PACKET_CLIENT_DENY_ALREADY_IN_PARTY) {
if (!printed_host_disconnect_) {
ScreenMessage(g_game->GetResourceString(
"connectionFailedHostAlreadyInPartyText"),
{1, 0, 0});
printed_host_disconnect_ = true;
}
} else {
if (!printed_host_disconnect_) {
ScreenMessage(g_game->GetResourceString("connectionRejectedText"),
{1, 0, 0});
printed_host_disconnect_ = true;
}
}
if (!keep_trying) {
PushDisconnectedFromHostCall();
}
}
}
break;
}
case BA_PACKET_CLIENT_REQUEST: {
if (data_size > 4) {
// Bytes 2 and 3 are their protocol ID, byte 4 is request ID, the rest
// is session-id.
uint16_t protocol_id;
memcpy(&protocol_id, data + 1, 2);
uint8_t request_id = data[3];
// They also send us their session-ID which should
// be completely unique to them; we can use this to lump client
// requests together and such.
std::vector<char> client_instance_buffer(data_size - 4 + 1);
memcpy(&(client_instance_buffer[0]), data + 4, data_size - 4);
client_instance_buffer[data_size - 4] = 0; // terminate string
std::string client_instance_uuid = &(client_instance_buffer[0]);
if (static_cast<int>(connections_to_clients_.size() + 1)
>= g_game->public_party_max_size()) {
// If we've reached our party size limit (including ourself in that
// count), reject.
// Newer version have a specific party-full message; send that first
// but also follow up with a generic deny message for older clients.
g_network_write_module->PushSendToCall(
{BA_PACKET_CLIENT_DENY_PARTY_FULL, request_id}, addr);
g_network_write_module->PushSendToCall(
{BA_PACKET_CLIENT_DENY, request_id}, addr);
} else if (connection_to_host_.exists()) {
// If we're connected to someone else, we can't have clients.
g_network_write_module->PushSendToCall(
{BA_PACKET_CLIENT_DENY_ALREADY_IN_PARTY, request_id}, addr);
} else {
// Otherwise go ahead and make them a new client connection.
Object::Ref<ConnectionToClientUDP> connection_to_client;
// Go through and see if we already have a client-connection for
// this request-id.
for (auto&& i : connections_to_clients_) {
if (ConnectionToClientUDP* cc_udp = i.second->GetAsUDP()) {
if (cc_udp->client_instance_uuid() == client_instance_uuid) {
connection_to_client = cc_udp;
break;
}
}
}
if (!connection_to_client.exists()) {
// Create them a client object.
// Try to find an unused client-id in the range 0-255.
int client_id = 0;
bool found = false;
for (int i = 0; i < 256; i++) {
int test_id = (next_connection_to_client_id_ + i) % 255;
if (connections_to_clients_.find(test_id)
== connections_to_clients_.end()) {
client_id = test_id;
found = true;
break;
}
}
next_connection_to_client_id_++;
// If all 255 slots are taken (whaaaaaaa?), reject them.
if (!found) {
std::vector<uint8_t> msg_out(2);
msg_out[0] = BA_PACKET_CLIENT_DENY;
msg_out[1] = request_id;
g_network_write_module->PushSendToCall(msg_out, addr);
Log("All client slots full; really?..");
break;
}
connection_to_client = Object::New<ConnectionToClientUDP>(
addr, client_instance_uuid, request_id, client_id);
connections_to_clients_[client_id] = connection_to_client;
}
// If we got to this point, regardless of whether
// we already had a connection or not, tell them
// they're accepted.
std::vector<uint8_t> msg_out(3);
msg_out[0] = BA_PACKET_CLIENT_ACCEPT;
assert(connection_to_client->id() < 256);
msg_out[1] =
static_cast_check_fit<uint8_t>(connection_to_client->id());
msg_out[2] = request_id;
g_network_write_module->PushSendToCall(msg_out, addr);
}
}
break;
}
default:
// Assuming we can get random other noise in here;
// should just silently ignore.
break;
}
}
void ConnectionSet::SetClientInfoFromMasterServer(
const std::string& client_token, PyObject* info_obj) {
// NOLINTNEXTLINE (python doing bitwise math on signed int)
if (!PyDict_Check(info_obj)) {
Log("got non-dict for master-server client info for token " + client_token
+ ": " + Python::ObjToString(info_obj));
return;
}
for (ConnectionToClient* client : GetConnectionsToClients()) {
if (client->token() == client_token) {
client->HandleMasterServerClientInfo(info_obj);
// Roster will now include account-id...
g_game->mark_game_roster_dirty();
break;
}
}
}
} // namespace ballistica

View File

@ -0,0 +1,762 @@
// Released under the MIT License. See LICENSE for details.
#include "ballistica/game/connection/connection_to_client.h"
// #include "ballistica/app/app_internal.h"
#include "ballistica/audio/audio.h"
#include "ballistica/game/client_controller_interface.h"
#include "ballistica/game/connection/connection_set.h"
#include "ballistica/game/game.h"
#include "ballistica/game/player.h"
#include "ballistica/game/session/host_session.h"
#include "ballistica/generic/json.h"
#include "ballistica/input/device/client_input_device.h"
#include "ballistica/media/media.h"
#include "ballistica/networking/networking.h"
#include "ballistica/python/python.h"
#include "ballistica/python/python_sys.h"
namespace ballistica {
// How long new clients have to wait before starting a kick vote.
const int kNewClientKickVoteDelay = 60000;
ConnectionToClient::ConnectionToClient(int id) : id_(id) {
// We calc this once just in case it changes on our end
// (the client uses it for their verification hash so we need to
// ensure it stays consistent).
our_handshake_player_spec_str_ =
PlayerSpec::GetAccountPlayerSpec().GetSpecString();
// On newer protocols we include an extra salt value
// to ensure the hash the client generates can't be recycled.
if (explicit_bool(kProtocolVersion >= 33)) {
our_handshake_salt_ = std::to_string(rand()); // NOLINT
}
}
auto ConnectionToClient::ShouldPrintIncompatibleClientErrors() const -> bool {
return false;
}
void ConnectionToClient::SetController(ClientControllerInterface* c) {
// If we had an old client-controller, inform it we're leaving it.
if (controller_) {
controller_->OnClientDisconnected(this);
controller_ = nullptr;
}
// If we've got a new one, connect it.
if (c) {
controller_ = c;
// We automatically push a session reset command before turning
// a client connection over to a new controller.
// The previous client may not have cleaned up after itself
// in cases such as truncated replays, etc.
SendReliableMessage(std::vector<uint8_t>(1, BA_MESSAGE_SESSION_RESET));
controller_->OnClientConnected(this);
}
}
ConnectionToClient::~ConnectionToClient() {
// If we've got a controller, disconnect from it.
SetController(nullptr);
// If we had made any input-devices, they're just pointers that
// we have to pass along to g_input to delete for us.
for (auto&& i : client_input_devices_) {
g_input->RemoveInputDevice(i.second, false);
}
// If they had been announced as connected, announce their departure.
if (can_communicate() && g_game->ShouldAnnouncePartyJoinsAndLeaves()) {
std::string s = g_game->GetResourceString("playerLeftPartyText");
Utils::StringReplaceOne(&s, "${NAME}", peer_spec().GetDisplayString());
ScreenMessage(s, {1, 0.5f, 0.0f});
g_audio->PlaySound(g_media->GetSound(SystemSoundID::kCorkPop));
}
}
void ConnectionToClient::Update() {
Connection::Update(); // Handles common stuff.
millisecs_t real_time = GetRealTime();
// If we're waiting for handshake response still, keep sending out handshake
// attempts.
if (!can_communicate() && real_time - last_hand_shake_send_time_ > 1000) {
// In newer protocols we embed a json dict as the second part of the
// handshake packet; this way we can evolve the protocol more
// easily in the future.
if (explicit_bool(kProtocolVersion >= 33)) {
// Construct a json dict with our player-spec-string as one element.
JsonDict dict;
dict.AddString("s", our_handshake_player_spec_str_);
// We also add our random salt for hashing.
dict.AddString("l", our_handshake_salt_);
std::string out = dict.PrintUnformatted();
std::vector<uint8_t> data(3 + out.size());
data[0] = BA_GAMEPACKET_HANDSHAKE;
uint16_t val = kProtocolVersion;
memcpy(data.data() + 1, &val, sizeof(val));
memcpy(data.data() + 3, out.c_str(), out.size());
SendGamePacket(data);
} else {
// (KILL THIS WHEN kProtocolVersionMin >= 33)
// on older protocols, we simply embedded our spec-string as the second
// part of the handshake packet
std::vector<uint8_t> data(3 + our_handshake_player_spec_str_.size());
data[0] = BA_GAMEPACKET_HANDSHAKE;
uint16_t val = kProtocolVersion;
memcpy(data.data() + 1, &val, sizeof(val));
memcpy(data.data() + 3, our_handshake_player_spec_str_.c_str(),
our_handshake_player_spec_str_.size());
SendGamePacket(data);
}
last_hand_shake_send_time_ = real_time;
}
}
void ConnectionToClient::HandleGamePacket(const std::vector<uint8_t>& data) {
// If we've errored, just respond to everything with 'GO AWAY!'.
if (errored()) {
std::vector<uint8_t> data2(1);
data2[0] = BA_GAMEPACKET_DISCONNECT;
SendGamePacket(data2);
return;
}
if (data.empty()) {
Log("Error: ConnectionToClient got data size 0.");
return;
}
switch (data[0]) {
case BA_GAMEPACKET_HANDSHAKE_RESPONSE: {
// We sent the client a handshake and they're responding.
if (data.size() < 3) {
Log("got invalid BA_GAMEPACKET_HANDSHAKE_RESPONSE");
return;
}
// In newer builds we expect to be sent a json dict here;
// pull client's spec from that.
if (explicit_bool(kProtocolVersion >= 33)) {
std::vector<char> string_buffer(data.size() - 3 + 1);
memcpy(&(string_buffer[0]), &(data[3]), data.size() - 3);
string_buffer[string_buffer.size() - 1] = 0;
cJSON* handshake = cJSON_Parse(string_buffer.data());
if (handshake) {
if (cJSON* pspec = cJSON_GetObjectItem(handshake, "s")) {
set_peer_spec(PlayerSpec(pspec->valuestring));
}
// Newer builds also send their public-device-id; servers
// can use this to combat simple spam attacks.
if (cJSON* pubdeviceid = cJSON_GetObjectItem(handshake, "d")) {
public_device_id_ = pubdeviceid->valuestring;
}
cJSON_Delete(handshake);
}
} else {
// (KILL THIS WHEN kProtocolVersionMin >= 33)
// older versions only contained the client spec
// pull client's spec from the handshake packet..
std::vector<char> string_buffer(data.size() - 3 + 1);
memcpy(&(string_buffer[0]), &(data[3]), data.size() - 3);
string_buffer[string_buffer.size() - 1] = 0;
set_peer_spec(PlayerSpec(&(string_buffer[0])));
}
// FIXME: We should maybe set some sort of 'pending' peer-spec
// and fetch their actual info from the master-server.
// (or at least make that an option for internet servers)
// Compare this against our blocked specs.. if there's a match, reject
// them.
if (g_game->IsPlayerBanned(peer_spec())) {
Error("");
return;
}
// Bytes 2 and 3 are their protocol version.
uint16_t val;
memcpy(&val, data.data() + 1, sizeof(val));
if (val != kProtocolVersion) {
// Depending on the connection type we may print the connection
// failure or not. (If we invited them it'd be good to know about the
// failure).
std::string s;
if (ShouldPrintIncompatibleClientErrors()) {
// If they get here, announce on the host that the client is
// incompatible. UDP connections will get rejected during the
// connection attempt so this will only apply to things like Google
// Play invites where we probably want to be more verbose as
// to why the game just died.
s = g_game->GetResourceString("incompatibleVersionPlayerText");
Utils::StringReplaceOne(&s, "${NAME}",
peer_spec().GetDisplayString());
}
Error(s);
return;
}
// At this point we know we speak their language so we can send
// them things beyond handshake packets.
if (!can_communicate()) {
set_can_communicate(true);
// Don't allow fresh clients to start kick votes for a while.
next_kick_vote_allow_time_ = GetRealTime() + kNewClientKickVoteDelay;
// At this point we have their name, so lets announce their arrival.
if (g_game->ShouldAnnouncePartyJoinsAndLeaves()) {
std::string s = g_game->GetResourceString("playerJoinedPartyText");
Utils::StringReplaceOne(&s, "${NAME}",
peer_spec().GetDisplayString());
ScreenMessage(s, {0.5f, 1, 0.5f});
g_audio->PlaySound(g_media->GetSound(SystemSoundID::kGunCock));
}
// Also mark the time for flashing the 'someone just joined your
// party' message in the corner.
g_game->set_last_connection_to_client_join_time(GetRealTime());
// Added midway through protocol 29:
// We now send a json dict of info about ourself first thing. This
// gives us a nice open-ended way to expand functionality/etc. going
// forward. The other end will expect that this is the first reliable
// message they get; if something else shows up first they'll assume
// we're an old build and not sending this.
{
cJSON* info_dict = cJSON_CreateObject();
cJSON_AddItemToObject(info_dict, "b",
cJSON_CreateNumber(kAppBuildNumber));
// Add a name entry if we've got a public party name set.
if (!g_game->public_party_name().empty()) {
cJSON_AddItemToObject(
info_dict, "n",
cJSON_CreateString(g_game->public_party_name().c_str()));
}
std::string info = cJSON_PrintUnformatted(info_dict);
cJSON_Delete(info_dict);
std::vector<uint8_t> info_msg(info.size() + 1);
info_msg[0] = BA_MESSAGE_HOST_INFO;
memcpy(&(info_msg[1]), info.c_str(), info.size());
SendReliableMessage(info_msg);
}
std::string joiner_spec = peer_spec().GetSpecString();
std::vector<uint8_t> join_msg(joiner_spec.size() + 1);
join_msg[0] = BA_MESSAGE_PARTY_MEMBER_JOINED;
memcpy(&(join_msg[1]), joiner_spec.c_str(), joiner_spec.size());
for (auto&& i : g_game->connections()->connections_to_clients()) {
// Also send a 'party-member-joined' notification to all clients
// *except* the new one.
if (i.second.exists() && i.second.get() != this
&& g_game->ShouldAnnouncePartyJoinsAndLeaves()) {
i.second->SendReliableMessage(join_msg);
}
}
// Update the game party roster and send it to all clients (including
// this new one).
g_game->UpdateGameRoster();
// Lastly, we hand this connection over to whoever is currently
// feeding client connections.
if (g_game->connections()->client_controller()) {
SetController(g_game->connections()->client_controller());
}
}
break;
}
default:
// Let our base class handle common stuff *if* we're connected.
if (can_communicate()) {
Connection::HandleGamePacket(data);
}
break;
}
}
void ConnectionToClient::Error(const std::string& msg) {
// Take no further action at this time aside from printing it.
// If we receive any more messages from the client we'll respond
// with a disconnect message in HandleGamePacket().
Connection::Error(msg); // Common stuff.
}
void ConnectionToClient::SendScreenMessage(const std::string& s, float r,
float g, float b) {
// Older clients don't support the screen-message message, so in that case
// we just send it as a chat-message from <HOST>.
if (build_number() < 14248) {
std::string value = g_game->CompileResourceString(s, "sendScreenMessage");
std::string our_spec_string =
PlayerSpec::GetDummyPlayerSpec("<HOST>").GetSpecString();
std::vector<uint8_t> msg_out(1 + 1 + our_spec_string.size() + value.size());
msg_out[0] = BA_MESSAGE_CHAT;
size_t spec_size = our_spec_string.size();
assert(spec_size < 256);
msg_out[1] = static_cast<uint8_t>(spec_size);
memcpy(&(msg_out[2]), our_spec_string.c_str(),
static_cast<size_t>(spec_size));
memcpy(&(msg_out[2 + spec_size]), value.c_str(), value.size());
SendReliableMessage(msg_out);
} else {
cJSON* msg = cJSON_CreateObject();
cJSON_AddNumberToObject(msg, "t", BA_JMESSAGE_SCREEN_MESSAGE);
cJSON_AddStringToObject(msg, "m", s.c_str());
cJSON_AddNumberToObject(msg, "r", r);
cJSON_AddNumberToObject(msg, "g", g);
cJSON_AddNumberToObject(msg, "b", b);
SendJMessage(msg);
cJSON_Delete(msg);
}
}
void ConnectionToClient::HandleMessagePacket(
const std::vector<uint8_t>& buffer) {
if (buffer.empty()) {
Log("Error: Got invalid HandleMessagePacket.");
return;
}
// If the first message we get is not client-info, it means we're talking to
// an older client that won't be sending us info.
if (!got_client_info_ && buffer[0] != BA_MESSAGE_CLIENT_INFO) {
build_number_ = 0;
got_client_info_ = true;
}
switch (buffer[0]) {
case BA_MESSAGE_JMESSAGE: {
if (buffer.size() >= 3 && buffer[buffer.size() - 1] == 0) {
cJSON* msg =
cJSON_Parse(reinterpret_cast<const char*>(buffer.data() + 1));
if (msg) {
cJSON_Delete(msg);
}
}
break;
}
case BA_MESSAGE_KICK_VOTE: {
if (buffer.size() == 2) {
for (auto&& i : g_game->connections()->connections_to_clients()) {
ConnectionToClient* client = i.second.get();
if (client->id() == static_cast<int>(buffer[1])) {
g_game->StartKickVote(this, client);
break;
}
}
}
break;
}
case BA_MESSAGE_CLIENT_INFO: {
if (buffer.size() > 1) {
std::vector<char> str_buffer(buffer.size());
memcpy(&(str_buffer[0]), &(buffer[1]), buffer.size() - 1);
str_buffer[str_buffer.size() - 1] = 0;
cJSON* info = cJSON_Parse(reinterpret_cast<const char*>(&(buffer[1])));
if (info) {
cJSON* b = cJSON_GetObjectItem(info, "b");
if (b) {
build_number_ = b->valueint;
} else {
Log("no buildnumber in clientinfo msg");
}
// Grab their token (we use this to ask the
// server for their v1 account info).
cJSON* t = cJSON_GetObjectItem(info, "tk");
if (t) {
token_ = t->valuestring;
} else {
Log("no token in clientinfo msg");
}
// Newer clients also pass a peer-hash, which
// we can include with the token to allow the
// v1 server to better verify the client's identity.
cJSON* ph = cJSON_GetObjectItem(info, "ph");
if (ph) {
peer_hash_ = ph->valuestring;
}
if (!token_.empty()) {
// Kick off a query to the master-server for this client's info.
// FIXME: we need to add retries for this in case of failure.
AppInternalClientInfoQuery(
token_, our_handshake_player_spec_str_ + our_handshake_salt_,
peer_hash_, build_number_);
}
cJSON_Delete(info);
} else {
Log("got invalid json in clientinfo message: '"
+ std::string(reinterpret_cast<const char*>(&(buffer[1]))) + "'");
}
}
got_client_info_ = true;
break;
}
case BA_MESSAGE_CLIENT_PLAYER_PROFILES_JSON: {
// Newer type using json.
// Only accept peer info if we've not gotten official info from
// the master server (and if we're allowing it in general).
if (!g_game->require_client_authentication()
&& !got_info_from_master_server_) {
std::vector<char> b2(buffer.size());
memcpy(&(b2[0]), &(buffer[1]), buffer.size() - 1);
b2[buffer.size() - 1] = 0;
PythonRef args(Py_BuildValue("(s)", b2.data()), PythonRef::kSteal);
PythonRef results =
g_python->obj(Python::ObjID::kJsonLoadsCall).Call(args);
if (results.exists()) {
player_profiles_ = results;
}
}
break;
}
case BA_MESSAGE_CLIENT_PLAYER_PROFILES: {
// Ok at this point we shouldn't attempt to eval these;
// they would have been sent in python 2 and we're python 3
// so they likely will fail in subtle ways.
// ('u' prefixes before unicode and this and that)
// Just gonna hope everyone is updated to a recent-ish version so
// we don't get these.
// This might be a good argument to separate out the protocol versions
// we support for game streams vs client-connections. We could disallow
// connections to/from these older peers while still allowing old replays
// to play back.
BA_LOG_ONCE("Received old pre-json player profiles msg; ignoring.");
break;
}
case BA_MESSAGE_CHAT: {
// We got a chat message from a client.
millisecs_t now = GetRealTime();
// Ignore this if they're chat blocked.
if (now >= chat_block_time_) {
// We keep track of their recent chat times.
// If they exceed a certain amount in the last several seconds,
// Institute a chat block.
last_chat_times_.push_back(now);
uint32_t timeSample = 5000;
if (now >= timeSample) {
while (!last_chat_times_.empty()
&& last_chat_times_[0] < now - timeSample) {
last_chat_times_.erase(last_chat_times_.begin());
}
}
// If we require client-info and don't have it from this guy yet,
// ignore their chat messages (prevent bots from jumping in and
// spamming before we can verify their identities)
if (g_game->require_client_authentication()
&& !got_info_from_master_server_) {
Log("Ignoring chat message from peer with no client info.");
SendScreenMessage(R"({"r":"loadingTryAgainText"})", 1, 0, 0);
} else if (last_chat_times_.size() >= 5) {
chat_block_time_ = now + next_chat_block_seconds_ * 1000;
g_game->connections()->SendScreenMessageToAll(
R"({"r":"internal.chatBlockedText","s":[["${NAME}",)"
+ Utils::GetJSONString(
GetCombinedSpec().GetDisplayString().c_str())
+ R"(],["${TIME}",")"
+ std::to_string(next_chat_block_seconds_) + "\"]]}",
1, 1, 0);
next_chat_block_seconds_ *= 2; // make it worse next time
} else {
// Send this along to all clients.
// *however* we want to ignore the player-spec that was included in
// the chat message and replace it with our own for this
// client-connection.
if (buffer.size() > 3) {
int spec_len = buffer[1];
auto msg_len = static_cast<int>(buffer.size() - spec_len - 2);
if (spec_len > 0 && msg_len >= 0) {
std::vector<char> b2(static_cast<size_t>(msg_len) + 1);
if (msg_len > 0) {
memcpy(&(b2[0]), &(buffer[2 + spec_len]),
static_cast<size_t>(msg_len));
}
b2[msg_len] = 0;
// Clamp messages at a reasonable size
// (yes, people used this to try and crash machines).
if (b2.size() > 100) {
SendScreenMessage(
"{\"t\":[\"serverResponses\","
"\"Message is too long.\"]}",
1, 0, 0);
} else if (g_game->kick_vote_in_progress()
&& (!strcmp(b2.data(), "1")
|| !strcmp(b2.data(), "2"))) {
// Special case - if there's a kick vote going on, take '1' or
// '2' to be votes.
// TODO(ericf): Disable this based on build-numbers once we've
// got GUI voting working.
if (!kick_voted_) {
kick_voted_ = true;
kick_vote_choice_ = !strcmp(b2.data(), "1");
} else {
SendScreenMessage(R"({"r":"votedAlreadyText"})", 1, 0, 0);
}
} else {
// Pass the message through any custom filtering we've got.
// If the filter tells us to ignore it, we're done.
std::string message = b2.data();
bool allow_message =
g_python->FilterChatMessage(&message, id());
if (!allow_message) {
break;
}
std::string spec_string = GetCombinedSpec().GetSpecString();
std::vector<uint8_t> msg_out(1 + 1 + spec_string.size()
+ message.size());
msg_out[0] = BA_MESSAGE_CHAT;
size_t spec_size = spec_string.size();
assert(spec_size < 256);
msg_out[1] = static_cast<unsigned char>(spec_size);
memcpy(&(msg_out[2]), spec_string.c_str(),
static_cast<size_t>(spec_size));
memcpy(&(msg_out[2 + spec_size]), message.c_str(),
message.size());
// Send it out to all clients.
for (auto&& i :
g_game->connections()->connections_to_clients()) {
if (i.second->can_communicate()) {
i.second->SendReliableMessage(msg_out);
}
}
// Display it locally.
g_game->LocalDisplayChatMessage(msg_out);
}
}
}
}
}
break;
}
case BA_MESSAGE_REMOTE_PLAYER_INPUT_COMMANDS: {
if (ClientInputDevice* client_input_device =
GetClientInputDevice(buffer[1])) {
int count = static_cast<int>((buffer.size() - 2) / 5);
if ((buffer.size() - 2) % 5 != 0) {
Log("Error: invalid player-input-commands packet");
break;
}
int index = 2;
for (int i = 0; i < count; i++) {
auto type = (InputType)buffer[index++];
float val;
memcpy(&val, &(buffer[index]), 4);
index += 4;
client_input_device->PassInputCommand(type, val);
}
}
break;
}
case BA_MESSAGE_REMOVE_REMOTE_PLAYER: {
last_remove_player_time_ = GetRealTime();
if (buffer.size() != 2) {
Log("Error: invalid remove-remote-player packet");
break;
}
if (ClientInputDevice* cid = GetClientInputDevice(buffer[1])) {
if (Player* player = cid->GetPlayer()) {
HostSession* host_session = player->GetHostSession();
if (!host_session) throw Exception("Player's host-session not found");
host_session->RemovePlayer(player);
}
}
break;
}
case BA_MESSAGE_REQUEST_REMOTE_PLAYER: {
if (buffer.size() != 2) {
Log("Error: invalid remote-player-request packet");
break;
}
// Create/fetch our client-input that represents this guy
// and submit a player-request on it's behalf.
ClientInputDevice* cid = GetClientInputDevice(buffer[1]);
if (auto* hs =
dynamic_cast<HostSession*>(g_game->GetForegroundSession())) {
if (!cid->attached_to_player()) {
millisecs_t seconds_since_last_left =
(GetRealTime() - last_remove_player_time_) / 1000;
int min_seconds_since_left = 10;
// If someone on this connection left less than 10 seconds ago,
// prevent them from immediately jumping back in.
if (seconds_since_last_left < min_seconds_since_left) {
SendScreenMessage(
"{\"t\":[\"serverResponses\",\"You can join in ${COUNT} "
"seconds.\"],\"s\":[[\"${COUNT}\",\""
+ std::to_string(min_seconds_since_left
- seconds_since_last_left)
+ "\"]]}",
1, 1, 0);
} else {
bool still_waiting = (g_game->require_client_authentication()
&& !got_info_from_master_server_);
// If we're not allowing peer client-info and have yet to get
// master-server info for this client, delay their join (we'll
// eventually give up and just give them a blank slate).
if (still_waiting && (GetRealTime() - creation_time() < 10000)) {
SendScreenMessage(
"{\"v\":\"${A}...\",\"s\":[[\"${A}\",{\"r\":"
"\"loadingTryAgainText\",\"f\":\"loadingText\"}]]}",
1, 1, 0);
} else {
// Either timed out or have info; let the request go through.
if (still_waiting) {
Log("Allowing player-request without client\'s master-server "
"info (build "
+ std::to_string(build_number_) + ")");
}
hs->RequestPlayer(cid);
}
}
}
} else {
Log("Error: ConnectionToClient got remote player"
" request but have no host session");
}
break;
}
default: {
// Hackers have attempted to mess with servers by sending huge amounts of
// data through chat messages/etc. Let's watch out for mutli-part messages
// growing too large and kick/ban the client if they do.
if (buffer[0] == BA_MESSAGE_MULTIPART) {
if (multipart_buffer_size() > 50000) {
// Its not actually unknown but shhh don't tell the hackers...
SendScreenMessage(R"({"r":"errorUnknownText"})", 1, 0, 0);
Log("Client data limit exceeded by '" + peer_spec().GetShortName()
+ "'; kicking.");
g_game->BanPlayer(peer_spec(), 1000 * 60);
Error("");
return;
}
}
Connection::HandleMessagePacket(buffer);
}
}
}
auto ConnectionToClient::GetCombinedSpec() -> PlayerSpec {
// Look for players coming from this client-connection.
// If we find any, make a spec out of their name(s).
if (auto* hs = dynamic_cast<HostSession*>(g_game->GetForegroundSession())) {
std::string p_name_combined;
for (auto&& p : hs->players()) {
InputDevice* input_device = p->GetInputDevice();
if (!p->GetName().empty() && p->name_is_real() && p->accepted()
&& input_device != nullptr && input_device->IsRemoteClient()) {
auto* cid = static_cast<ClientInputDevice*>(input_device);
ConnectionToClient* ctc = cid->connection_to_client();
// Add some basic info for each remote player.
if (ctc != nullptr && ctc == this) {
if (!p_name_combined.empty()) {
p_name_combined += "/";
}
p_name_combined += p->GetName();
}
}
}
if (p_name_combined.size() > kMaxPartyNameCombinedSize) {
p_name_combined.resize(kMaxPartyNameCombinedSize);
p_name_combined += "...";
}
if (!p_name_combined.empty()) {
return PlayerSpec::GetDummyPlayerSpec(p_name_combined);
}
}
// Welp, that didn't work.
// As a fallback, just use the peer spec (account name, etc.)
return peer_spec();
}
auto ConnectionToClient::GetClientInputDevice(int remote_id)
-> ClientInputDevice* {
auto i = client_input_devices_.find(remote_id);
if (i == client_input_devices_.end()) {
// InputDevices need to be manually allocated and passed to g_input to
// store.
auto cid = Object::NewDeferred<ClientInputDevice>(remote_id, this);
client_input_devices_[remote_id] = cid;
g_input->AddInputDevice(cid, false);
return cid;
}
return i->second;
}
auto ConnectionToClient::GetAsUDP() -> ConnectionToClientUDP* {
return nullptr;
}
void ConnectionToClient::HandleMasterServerClientInfo(PyObject* info_obj) {
PyObject* profiles_obj = PyDict_GetItemString(info_obj, "p");
if (profiles_obj != nullptr) {
player_profiles_.Acquire(profiles_obj);
}
// This will also contain a public account-id (if the query was valid).
// Store it away for whoever wants it.
PyObject* public_id_obj = PyDict_GetItemString(info_obj, "u");
if (public_id_obj != nullptr && Python::IsPyString(public_id_obj)) {
peer_public_account_id_ = Python::GetPyString(public_id_obj);
} else {
peer_public_account_id_ = "";
// If the server returned no valid account info for them
// and we're not trusting peers, kick this fella right out
// and ban him for a short bit (to hopefully limit rejoin spam).
if (g_game->require_client_authentication()) {
SendScreenMessage(
"{\"t\":[\"serverResponses\","
"\"Your account was rejected. Are you signed in?\"]}",
1, 0, 0);
Log("Master server found no valid account for '"
+ peer_spec().GetShortName() + "'; kicking.");
// Not benning anymore. People were exploiting this by impersonating
// other players using their public ids to get them banned from
// their own servers/etc.
// g_game->BanPlayer(peer_spec(), 1000 * 60);
Error("");
}
}
got_info_from_master_server_ = true;
}
auto ConnectionToClient::IsAdmin() const -> bool {
if (peer_public_account_id_.empty()) {
return false;
}
return (g_game->admin_public_ids().find(peer_public_account_id_)
!= g_game->admin_public_ids().end());
}
} // namespace ballistica

View File

@ -0,0 +1,95 @@
// Released under the MIT License. See LICENSE for details.
#include "ballistica/game/connection/connection_to_client_udp.h"
#include "ballistica/game/connection/connection_set.h"
#include "ballistica/game/game.h"
#include "ballistica/networking/network_write_module.h"
#include "ballistica/networking/sockaddr.h"
namespace ballistica {
ConnectionToClientUDP::ConnectionToClientUDP(const SockAddr& addr,
std::string client_name,
uint8_t request_id, int client_id)
: ConnectionToClient(client_id),
request_id_(request_id),
addr_(new SockAddr(addr)),
client_instance_uuid_(std::move(client_name)),
last_client_response_time_(g_game->master_time()),
did_die_(false) {}
ConnectionToClientUDP::~ConnectionToClientUDP() {
// This prevents anything from trying to send
// (and thus crashing in pure-virtual SendGamePacketCompressed) as we die.
set_connection_dying(true);
}
void ConnectionToClientUDP::SendGamePacketCompressed(
const std::vector<uint8_t>& data) {
// Ok, we've got a random chunk of (possibly) compressed data to send over
// the wire.. lets stick a header on it and ship it out.
std::vector<uint8_t> data_full(data.size() + 2);
memcpy(&(data_full[2]), &data[0], data.size());
data_full[0] = BA_PACKET_HOST_GAMEPACKET_COMPRESSED;
// Go ahead and include their original request_id so they know we're talking
// to them.
data_full[1] = request_id_;
// Ship this off to the net-out thread to send; at this point we don't know
// or case what happens to it.
assert(g_network_write_module);
g_network_write_module->PushSendToCall(data_full, *addr_);
}
void ConnectionToClientUDP::Update() {
ConnectionToClient::Update();
millisecs_t current_time = g_game->master_time();
// if its been long enough since we've heard anything from the host, error.
if (current_time - last_client_response_time_
> (can_communicate() ? 10000u : 5000u)) {
// die immediately in this case; no use trying to wait for a
// disconnect-ack since we've already given up hope of hearing from them..
Die();
return;
}
}
void ConnectionToClientUDP::HandleGamePacket(
const std::vector<uint8_t>& buffer) {
// keep track of when we last heard from the host for disconnect purposes
last_client_response_time_ = g_game->master_time();
ConnectionToClient::HandleGamePacket(buffer);
}
void ConnectionToClientUDP::Die() {
if (did_die_) {
Log("Error: Posting multiple die messages; probably not good.");
return;
}
// this will actually clear the object..
g_game->connections()->PushClientDisconnectedCall(id());
did_die_ = true;
}
auto ConnectionToClientUDP::GetAsUDP() -> ConnectionToClientUDP* {
return this;
}
void ConnectionToClientUDP::RequestDisconnect() {
// mark us as errored so all future communication results in more disconnect
// requests
set_errored(true);
SendDisconnectRequest();
}
void ConnectionToClientUDP::SendDisconnectRequest() {
std::vector<uint8_t> data(2);
data[0] = BA_PACKET_DISCONNECT_FROM_HOST_REQUEST;
data[1] = static_cast<uint8_t>(id());
g_network_write_module->PushSendToCall(data, *addr_);
}
} // namespace ballistica

View File

@ -0,0 +1,494 @@
// Released under the MIT License. See LICENSE for details.
#include "ballistica/game/connection/connection_to_host.h"
#include "ballistica/audio/audio.h"
#include "ballistica/game/game.h"
#include "ballistica/game/session/net_client_session.h"
#include "ballistica/generic/json.h"
#include "ballistica/input/device/input_device.h"
#include "ballistica/input/input.h"
#include "ballistica/math/vector3f.h"
#include "ballistica/media/media.h"
#include "ballistica/networking/networking.h"
#include "ballistica/platform/platform.h"
#include "ballistica/python/python.h"
#include "ballistica/python/python_sys.h"
namespace ballistica {
ConnectionToHost::ConnectionToHost() = default;
auto ConnectionToHost::GetAsUDP() -> ConnectionToHostUDP* { return nullptr; }
ConnectionToHost::~ConnectionToHost() {
// If we were considered 'connected', announce that we're leaving.
if (can_communicate()) {
// If we've already printed a 'connected' message, print 'disconnected'.
// Otherwise say the connection was rejected.
if (printed_connect_message_) {
// Use the party/game name if we've got it; otherwise say
// '${PEER-NAME}'s party'.
std::string s;
if (!party_name_.empty()) {
s = g_game->GetResourceString("leftGameText");
Utils::StringReplaceOne(&s, "${NAME}", party_name_);
} else {
s = g_game->GetResourceString("leftPartyText");
Utils::StringReplaceOne(&s, "${NAME}", peer_spec().GetDisplayString());
}
ScreenMessage(s, {1, 0.5f, 0.0f});
g_audio->PlaySound(g_media->GetSound(SystemSoundID::kCorkPop));
} else {
ScreenMessage(g_game->GetResourceString("connectionRejectedText"),
{1, 0, 0});
}
}
}
void ConnectionToHost::Update() { Connection::Update(); }
// Seems we get a false alarm here.
#pragma clang diagnostic push
#pragma ide diagnostic ignored "LocalValueEscapesScope"
void ConnectionToHost::HandleGamePacket(const std::vector<uint8_t>& data) {
// If we've errored, ignore everything; we're just a zombie.
if (errored()) {
// Hmmm; do we want to respond with disconnect packets here?
// (not remembering why server side does that but we don't).
return;
}
if (data.empty()) {
return;
}
switch (data[0]) {
case BA_GAMEPACKET_HANDSHAKE: {
if (data.size() <= 3) {
break;
}
// We expect a > 3 byte handshake packet with protocol version as the
// second and third bytes and name/info beyond that.
// (player-spec for protocol <= 32 and info json dict for 33+).
// If we don't support their protocol, let them know..
bool compatible = false;
uint16_t their_protocol_version;
memcpy(&their_protocol_version, data.data() + 1,
sizeof(their_protocol_version));
if (their_protocol_version >= kProtocolVersionMin
&& their_protocol_version <= kProtocolVersion) {
compatible = true;
// If we are compatible, set our protocol version to match
// what they're dealing.
protocol_version_ = their_protocol_version;
}
// Ok now we know if we can talk to them. Respond so they know
// whether they can talk to us.
// (packet-type, our protocol-version, our spec/info)
// For server-protocol < 32 we provide our player-spec.
// For server-protocol 33+ we provide json info dict.
if (their_protocol_version >= 33) {
// Construct a json dict with our player-spec-string as one element
JsonDict dict;
dict.AddString("s", PlayerSpec::GetAccountPlayerSpec().GetSpecString());
// Also add our public device id. Servers can
// use this to combat spammers.
dict.AddString("d", g_platform->GetPublicDeviceUUID());
std::string out = dict.PrintUnformatted();
std::vector<uint8_t> data2(3 + out.size());
data2[0] = BA_GAMEPACKET_HANDSHAKE_RESPONSE;
auto val = static_cast<uint16_t>(protocol_version_);
memcpy(data2.data() + 1, &val, sizeof(val));
memcpy(data2.data() + 3, out.c_str(), out.size());
SendGamePacket(data2);
} else {
// (KILL THIS WHEN kProtocolVersionMin >= 33)
std::string our_spec_str =
PlayerSpec::GetAccountPlayerSpec().GetSpecString();
std::vector<uint8_t> response(3 + our_spec_str.size());
response[0] = BA_GAMEPACKET_HANDSHAKE_RESPONSE;
auto val = static_cast<uint16_t>(protocol_version_);
memcpy(response.data() + 1, &val, sizeof(val));
memcpy(response.data() + 3, our_spec_str.c_str(), our_spec_str.size());
SendGamePacket(response);
}
if (!compatible) {
if (their_protocol_version > kProtocolVersion) {
Error(g_game->GetResourceString("incompatibleNewerVersionHostText"));
} else {
Error(g_game->GetResourceString("incompatibleVersionHostText"));
}
return;
}
// If we're freshly establishing that we're able to talk to them
// in a language they understand, go ahead and kick some stuff off.
if (!can_communicate()) {
if (their_protocol_version >= 33) {
// In newer protocols, handshake contains a json dict
// so we can evolve it going forward.
std::vector<char> string_buffer(data.size() - 3 + 1);
memcpy(&(string_buffer[0]), &(data[3]), data.size() - 3);
string_buffer[string_buffer.size() - 1] = 0;
cJSON* handshake = cJSON_Parse(string_buffer.data());
if (handshake) {
// We hash this to prove that we're us; keep it around.
peer_hash_input_ = "";
cJSON* pspec = cJSON_GetObjectItem(handshake, "s");
if (pspec) {
peer_hash_input_ += pspec->valuestring;
set_peer_spec(PlayerSpec(pspec->valuestring));
}
cJSON* salt = cJSON_GetObjectItem(handshake, "l");
if (salt) {
peer_hash_input_ += salt->valuestring;
}
cJSON_Delete(handshake);
}
} else {
// (KILL THIS WHEN kProtocolVersionMin >= 33)
// In older protocols, handshake simply contained a
// player-spec for the host.
// Pull host's PlayerSpec from the handshake packet.
std::vector<char> string_buffer(data.size() - 3 + 1);
memcpy(&(string_buffer[0]), &(data[3]), data.size() - 3);
string_buffer[string_buffer.size() - 1] = 0;
// We hash this to prove that we're us; keep it around.
peer_hash_input_ = string_buffer.data();
set_peer_spec(PlayerSpec(string_buffer.data()));
}
peer_hash_ = AppInternalCalcV1PeerHash(peer_hash_input_);
set_can_communicate(true);
g_game->LaunchClientSession();
// NOTE:
// we don't actually print a 'connected' message until after
// we get our first message (it may influence the message we print and
// there's also a chance we could still get booted after sending our
// info message)
// Wire ourselves up to drive the client-session we're in.
auto* cs =
dynamic_cast<NetClientSession*>(g_game->GetForegroundSession());
assert(cs);
assert(!cs->connection_to_host());
client_session_ = cs;
cs->SetConnectionToHost(this);
// The very first thing we send is our client-info
// which is a json dict with arbitrary data.
{
JsonDict dict;
dict.AddNumber("b", kAppBuildNumber);
AppInternalV1SetClientInfo(&dict);
// Pass the hash we generated from their handshake; they can use
// this to make sure we're who we say we are.
dict.AddString("ph", peer_hash_);
std::string info = dict.PrintUnformatted();
std::vector<uint8_t> msg(info.size() + 1);
msg[0] = BA_MESSAGE_CLIENT_INFO;
memcpy(&(msg[1]), info.c_str(), info.size());
SendReliableMessage(msg);
}
// Send them our player-profiles so we can use them on their end.
// (the host generally will pull these from the master server
// to prevent cheating, but in some cases these are used)
// On newer hosts we send these as json.
if (protocol_version_ >= 32) {
// (This is a borrowed ref)
PyObject* profiles = g_python->GetRawConfigValue("Player Profiles");
PythonRef empty_dict;
if (!profiles) {
Log("No profiles found; sending empty list to host");
empty_dict.Steal(PyDict_New());
profiles = empty_dict.get();
}
if (profiles != nullptr) {
// Dump them to a json string.
PythonRef args(Py_BuildValue("(O)", profiles), PythonRef::kSteal);
PythonRef keywds(Py_BuildValue("{s(ss)}", "separators", ",", ":"),
PythonRef::kSteal);
PythonRef results =
g_python->obj(Python::ObjID::kJsonDumpsCall).Call(args, keywds);
if (!results.exists()) {
Log("Error getting json dump of local profiles");
} else {
try {
// Pull the string as utf8 and send.
std::string s = results.ValueAsString();
std::vector<uint8_t> msg(s.size() + 1);
msg[0] = BA_MESSAGE_CLIENT_PLAYER_PROFILES_JSON;
memcpy(&(msg[1]), &s[0], s.size());
SendReliableMessage(msg);
} catch (const std::exception& e) {
Log(std::string("exc sending player profiles to host: ")
+ e.what());
}
}
}
} else {
Log("Connected to old protocol; can't send player profiles");
}
}
break;
}
case BA_GAMEPACKET_DISCONNECT: {
// They told us to leave, so lets do so :-(
ErrorSilent();
break;
}
default:
// Let our base class handle common stuff *if* we're connected.
if (can_communicate()) {
Connection::HandleGamePacket(data);
}
break;
}
}
#pragma clang diagnostic pop
void ConnectionToHost::HandleMessagePacket(const std::vector<uint8_t>& buffer) {
assert(InGameThread());
if (buffer.empty()) {
Log("Error: got invalid HandleMessagePacket");
return;
}
// If the first message we get is not host-info, it means we're talking to
// an older host that won't be sending us info.
if (!got_host_info_ && buffer[0] != BA_MESSAGE_HOST_INFO) {
build_number_ = 0;
got_host_info_ = true;
}
switch (buffer[0]) {
case BA_MESSAGE_HOST_INFO: {
if (buffer.size() > 1) {
std::vector<char> str_buffer(buffer.size());
memcpy(&(str_buffer[0]), &(buffer[1]), buffer.size() - 1);
str_buffer[str_buffer.size() - 1] = 0;
cJSON* info = cJSON_Parse(reinterpret_cast<const char*>(&(buffer[1])));
if (info) {
// Build number.
cJSON* b = cJSON_GetObjectItem(info, "b");
if (b) {
build_number_ = b->valueint;
} else {
Log("no buildnumber in hostinfo msg");
}
// Party name.
cJSON* n = cJSON_GetObjectItem(info, "n");
if (n != nullptr) {
party_name_ = Utils::GetValidUTF8(n->valuestring, "bsmhi");
}
cJSON_Delete(info);
} else {
Log("got invalid json in hostinfo message");
}
}
got_host_info_ = true;
break;
}
case BA_MESSAGE_PARTY_ROSTER: {
if (buffer.size() >= 3 && buffer[buffer.size() - 1] == 0) {
// Expand this into a json object; if it's valid, replace the game's
// current roster with it.
cJSON* new_roster =
cJSON_Parse(reinterpret_cast<const char*>(&(buffer[1])));
if (new_roster) {
g_game->SetGameRoster(new_roster);
}
}
break;
}
case BA_MESSAGE_JMESSAGE: {
// High level json messages (nice and easy to expand on but not
// especially efficient).
if (buffer.size() >= 3 && buffer[buffer.size() - 1] == 0) {
cJSON* msg =
cJSON_Parse(reinterpret_cast<const char*>(buffer.data() + 1));
if (msg) {
cJSON* type = cJSON_GetObjectItem(msg, "t");
if (type != nullptr) {
switch (type->valueint) {
case BA_JMESSAGE_SCREEN_MESSAGE: {
std::string m;
float r = 1.0f;
float g = 1.0f;
float b = 1.0f;
if (cJSON* r_obj = cJSON_GetObjectItem(msg, "r")) {
r = static_cast<float>(r_obj->valuedouble);
}
if (cJSON* g_obj = cJSON_GetObjectItem(msg, "g")) {
g = static_cast<float>(g_obj->valuedouble);
}
if (cJSON* b_obj = cJSON_GetObjectItem(msg, "b")) {
b = static_cast<float>(b_obj->valuedouble);
}
if (cJSON* m_obj = cJSON_GetObjectItem(msg, "m")) {
m = m_obj->valuestring;
ScreenMessage(m, {r, g, b});
}
break;
}
default:
break;
}
}
cJSON_Delete(msg);
}
}
break;
}
case BA_MESSAGE_PARTY_MEMBER_JOINED: {
if (buffer.size() > 1) {
std::vector<char> str_buffer(buffer.size());
memcpy(&(str_buffer[0]), &(buffer[1]), buffer.size() - 1);
str_buffer[str_buffer.size() - 1] = 0;
std::string s = g_game->GetResourceString("playerJoinedPartyText");
Utils::StringReplaceOne(
&s, "${NAME}", PlayerSpec(str_buffer.data()).GetDisplayString());
ScreenMessage(s, {0.5f, 1.0f, 0.5f});
g_audio->PlaySound(g_media->GetSound(SystemSoundID::kGunCock));
}
break;
}
case BA_MESSAGE_PARTY_MEMBER_LEFT: {
// Host is informing us that someone in the party left.
if (buffer.size() > 1) {
std::vector<char> str_buffer(buffer.size());
memcpy(&(str_buffer[0]), &(buffer[1]), buffer.size() - 1);
str_buffer[str_buffer.size() - 1] = 0;
std::string s = g_game->GetResourceString("playerLeftPartyText");
Utils::StringReplaceOne(
&s, "${NAME}", PlayerSpec(&(str_buffer[0])).GetDisplayString());
ScreenMessage(s, {1, 0.5f, 0.0f});
g_audio->PlaySound(g_media->GetSound(SystemSoundID::kCorkPop));
}
break;
}
case BA_MESSAGE_ATTACH_REMOTE_PLAYER_2: {
// New-style packet which includes a 32-bit player_id.
if (buffer.size() != 6) {
Log("Error: invalid attach-remote-player-2 msg");
break;
}
// Grab this local input-device and tell it its controlling something on
// the host.
InputDevice* input_device = g_input->GetInputDevice(buffer[1]);
if (input_device) {
uint32_t player_id;
memcpy(&player_id, &(buffer[2]), sizeof(player_id));
input_device->AttachToRemotePlayer(
this, static_cast_check_fit<int>(player_id));
}
// Once we've gotten one of these we know to ignore the old style.
ignore_old_attach_remote_player_packets_ = true;
break;
}
case BA_MESSAGE_ATTACH_REMOTE_PLAYER: {
// If our server uses the newer ones, we should ignore these.
if (!ignore_old_attach_remote_player_packets_) {
// This message was used in older versions but is flawed in that
// player-id is an 8 bit value which isn't enough for longstanding
// public servers.
// TODO(ericf): can remove this once back-compat-protocol > 29.
if (buffer.size() != 3) {
Log("Error: Invalid attach-remote-player msg.");
break;
}
// Grab this local input-device and tell it its controlling something
// on the host.
InputDevice* input_device = g_input->GetInputDevice(buffer[1]);
if (input_device) {
input_device->AttachToRemotePlayer(this, buffer[2]);
}
}
break;
}
case BA_MESSAGE_CHAT: {
g_game->LocalDisplayChatMessage(buffer);
break;
}
case BA_MESSAGE_DETACH_REMOTE_PLAYER: {
if (buffer.size() != 2) {
Log("Error: Invalid detach-remote-player msg");
break;
}
InputDevice* input_device = g_input->GetInputDevice(buffer[1]);
if (input_device && input_device->GetRemotePlayer() == this)
input_device->DetachFromPlayer();
break;
}
case BA_MESSAGE_SESSION_COMMANDS:
case BA_MESSAGE_SESSION_RESET:
case BA_MESSAGE_SESSION_DYNAMICS_CORRECTION: {
// These commands are consumed directly by the session.
if (client_session_.exists()) {
client_session_->HandleSessionMessage(buffer);
}
break;
}
default: {
Connection::HandleMessagePacket(buffer);
}
}
// After we get our first message from the server is when we print our
// 'connected to XXX' message.
if (!printed_connect_message_) {
std::string s;
// If we've got a name for their party, use it; otherwise call it
// '${NAME}'s party'.
if (!party_name_.empty()) {
s = g_game->GetResourceString("connectedToGameText");
Utils::StringReplaceOne(&s, "${NAME}", party_name_);
} else {
s = g_game->GetResourceString("connectedToPartyText");
Utils::StringReplaceOne(&s, "${NAME}", peer_spec().GetDisplayString());
}
ScreenMessage(s, {0.5f, 1, 0.5f});
g_audio->PlaySound(g_media->GetSound(SystemSoundID::kGunCock));
printed_connect_message_ = true;
}
}
} // namespace ballistica

View File

@ -0,0 +1,185 @@
// Released under the MIT License. See LICENSE for details.
#include "ballistica/game/connection/connection_to_host_udp.h"
#include "ballistica/game/connection/connection_set.h"
#include "ballistica/game/game.h"
#include "ballistica/math/vector3f.h"
#include "ballistica/networking/network_write_module.h"
#include "ballistica/networking/sockaddr.h"
namespace ballistica {
auto ConnectionToHostUDP::SwitchProtocol() -> bool {
if (protocol_version() > kProtocolVersionMin) {
set_protocol_version(protocol_version() - 1);
// Need a new request id so we ignore further responses to our previous
// requests.
GetRequestID();
return true;
}
return false;
}
ConnectionToHostUDP::ConnectionToHostUDP(const SockAddr& addr)
: addr_(new SockAddr(addr)),
client_id_(-1),
last_client_id_request_time_(0),
last_disconnect_request_time_(0),
did_die_(false),
last_host_response_time_(g_game->master_time()) {
GetRequestID();
if (g_game->connections()->GetPrintUDPConnectProgress()) {
ScreenMessage(g_game->GetResourceString("connectingToPartyText"));
}
}
ConnectionToHostUDP::~ConnectionToHostUDP() {
// This prevents anything from trying to send (and thus crashing in
// pure-virtual SendGamePacketCompressed) as we die.
set_connection_dying(true);
}
void ConnectionToHostUDP::GetRequestID() {
// We store a unique-ish request ID to minimize the chance that data for
// previous connections/etc will muck with us.
// Try to start this value at something that won't be common in packets to
// minimize chance of garbage packets causing trouble.
static auto next_request_id =
static_cast<uint8_t>(71 + (rand() % 151)); // NOLINT
request_id_ = next_request_id++;
}
void ConnectionToHostUDP::Update() {
ConnectionToHost::Update();
millisecs_t current_time = g_game->master_time();
// If we've not gotten a client_id from the host yet, keep pestering it.
if (!errored()) {
if (client_id_ == -1 && current_time - last_client_id_request_time_ > 500) {
last_client_id_request_time_ = current_time;
// Client request packet: contains our protocol version (2 bytes), our
// request id (1 byte), and our session-identifier (remainder of the
// message).
const std::string& uuid{GetAppInstanceUUID()};
std::vector<uint8_t> msg(4 + uuid.size());
msg[0] = BA_PACKET_CLIENT_REQUEST;
auto p_version = static_cast<uint16_t>(protocol_version());
memcpy(&(msg[1]), &p_version, 2);
msg[3] = request_id_;
memcpy(&(msg[4]), uuid.c_str(), uuid.size());
g_network_write_module->PushSendToCall(msg, *addr_);
}
}
// If its been long enough since we've heard anything from the host, error.
if (current_time - last_host_response_time_
> (can_communicate() ? 10000u : 5000u)) {
// If the connection never got established, announce it failed.
if (!can_communicate()) {
ScreenMessage(g_game->GetResourceString("connectionFailedText"),
{1, 0, 0});
}
// Die immediately in this case; no use trying to wait for a disconnect-ack
// since we've already given up hope of hearing from them.
Die();
return;
} else if (errored()) {
// If we've errored, keep sending disconnect-requests periodically.
// Once we get a response (or time out in the above code) we'll die.
if (current_time - last_disconnect_request_time_ > 1000) {
last_disconnect_request_time_ = current_time;
// If we haven't even got a client id yet, we can't send disconnect
// requests; just die.
if (client_id_ == -1) {
Die();
return;
} else {
SendDisconnectRequest();
}
}
}
}
// Tells the game to actually kill us. We try to inform the server of our
// departure before doing this when possible.
void ConnectionToHostUDP::Die() {
if (did_die_) {
Log("Error: posting multiple die messages; probably not good.");
return;
}
if (g_game->connections()->connection_to_host() == this) {
g_game->connections()->PushDisconnectedFromHostCall();
did_die_ = true;
} else {
Log("Error: Running update for non-current host-connection; shouldn't "
"happen.");
}
}
void ConnectionToHostUDP::SendDisconnectRequest() {
assert(client_id_ != -1);
if (client_id_ != -1) {
std::vector<uint8_t> data(2);
data[0] = BA_PACKET_DISCONNECT_FROM_CLIENT_REQUEST;
data[1] = static_cast_check_fit<uint8_t>(client_id_);
g_network_write_module->PushSendToCall(data, *addr_);
}
}
void ConnectionToHostUDP::HandleGamePacket(const std::vector<uint8_t>& buffer) {
// Keep track of when we last heard from the host for time-out purposes.
last_host_response_time_ = g_game->master_time();
ConnectionToHost::HandleGamePacket(buffer);
}
void ConnectionToHostUDP::SendGamePacketCompressed(
const std::vector<uint8_t>& data) {
assert(!data.empty());
// Ok, we've got a random chunk of (possibly) compressed data to send over
// the wire. Lets stick a header on it and ship it out.
std::vector<uint8_t> data_full(data.size() + 2);
memcpy(&(data_full[2]), &data[0], data.size());
data_full[0] = BA_PACKET_CLIENT_GAMEPACKET_COMPRESSED;
data_full[1] = static_cast_check_fit<uint8_t>(client_id_);
// Ship this off to the net-out thread to send; at this point we don't know
// or care what happens to it.
assert(g_network_write_module);
g_network_write_module->PushSendToCall(data_full, *addr_);
}
void ConnectionToHostUDP::Error(const std::string& msg) {
// On our initial erroring, send a disconnect request immediately if we've
// got an ID otherwise just kill ourselves instantly.
if (!errored()) {
if (client_id_ != -1) {
SendDisconnectRequest();
} else {
Die();
}
}
// Common error stuff.
ConnectionToHost::Error(msg);
}
auto ConnectionToHostUDP::GetAsUDP() -> ConnectionToHostUDP* { return this; }
void ConnectionToHostUDP::RequestDisconnect() {
// Mark us as errored so all future communication results in more disconnect
// requests.
set_errored(true);
if (client_id_ != -1) {
SendDisconnectRequest();
}
}
} // namespace ballistica

View File

@ -0,0 +1,493 @@
// Released under the MIT License. See LICENSE for details.
#include "ballistica/networking/network_reader.h"
#include "ballistica/game/connection/connection_set.h"
#include "ballistica/game/game.h"
#include "ballistica/game/player_spec.h"
#include "ballistica/generic/json.h"
#include "ballistica/input/remote_app.h"
#include "ballistica/math/vector3f.h"
#include "ballistica/networking/network_write_module.h"
#include "ballistica/networking/sockaddr.h"
#include "ballistica/platform/platform.h"
#include "ballistica/python/python.h"
namespace ballistica {
NetworkReader::NetworkReader(int port) : port4_(port), port6_(port) {
thread_ = new std::thread(RunThreadStatic, this);
assert(g_network_reader == nullptr);
g_network_reader = this;
}
auto NetworkReader::Pause() -> void {
assert(InMainThread());
assert(!paused_);
{
std::unique_lock<std::mutex> lock(paused_mutex_);
paused_ = true;
}
// Ok now attempt to send a quick ping to ourself to wake us up so we can kill
// our socket.
if (port4_ != -1) {
PokeSelf();
} else {
Log("Error: NetworkReader port is -1 on pause");
}
}
void NetworkReader::Resume() {
assert(InMainThread());
assert(paused_);
{
std::unique_lock<std::mutex> lock(paused_mutex_);
paused_ = false;
}
// Poke our thread so it can go on its way.
paused_cv_.notify_all();
}
void NetworkReader::PokeSelf() {
int sd = socket(AF_INET, SOCK_DGRAM, 0);
if (sd < 0) {
Log("ERROR: unable to create sleep ping socket; errno "
+ g_platform->GetSocketErrorString());
} else {
struct sockaddr_in serv_addr {};
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // NOLINT
serv_addr.sin_port = 0; // any
int bresult = ::bind(sd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
if (bresult == 1) {
Log("ERROR: unable to bind sleep socket: "
+ g_platform->GetSocketErrorString());
} else {
struct sockaddr_in t_addr {};
memset(&t_addr, 0, sizeof(t_addr));
t_addr.sin_family = AF_INET;
t_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // NOLINT
t_addr.sin_port = htons(port4_); // NOLINT
char b[1] = {BA_PACKET_POKE};
ssize_t sresult =
sendto(sd, b, 1, 0, (struct sockaddr*)(&t_addr), sizeof(t_addr));
if (sresult == -1) {
Log("Error on sleep self-sendto: "
+ g_platform->GetSocketErrorString());
}
}
g_platform->CloseSocket(sd);
}
}
auto NetworkReader::RunThread() -> int {
if (!HeadlessMode()) {
remote_server_ = std::make_unique<RemoteAppServer>();
}
// Do this whole thing in a loop. If we get put to sleep we just start over.
while (true) {
// Sleep until we're unpaused.
if (paused_) {
std::unique_lock<std::mutex> lock(paused_mutex_);
paused_cv_.wait(lock, [this] { return (!paused_); });
}
{
// This needs to be locked during any socket-descriptor changes/writes.
std::lock_guard<std::mutex> lock(sd_mutex_);
int result;
int print_port_unavailable = false;
int initial_requested_port = port4_;
sd4_ = socket(AF_INET, SOCK_DGRAM, 0);
if (sd4_ < 0) {
Log("ERROR: Unable to open host socket; errno "
+ g_platform->GetSocketErrorString());
} else {
g_platform->SetSocketNonBlocking(sd4_);
// Bind to local server port.
struct sockaddr_in serv_addr {};
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // NOLINT
// Try our requested port for v4, then go with any available if that
// doesn't work.
serv_addr.sin_port = htons(port4_); // NOLINT
result = ::bind(sd4_, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
if (result != 0) {
// If we're headless then we abort here; we're useless if we don't get
// the port we wanted.
if (HeadlessMode()) {
Log("FATAL ERROR: unable to bind to requested udp port "
+ std::to_string(port4_) + " (ipv4)");
exit(1);
}
// Primary ipv4 bind failed; try on any port as a backup.
print_port_unavailable = true;
serv_addr.sin_port = htons(0); // NOLINT
result =
::bind(sd4_, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
// Wuh oh; no ipv6 for us i guess.
if (result != 0) {
g_platform->CloseSocket(sd4_);
sd4_ = -1;
}
}
}
// See what v4 port we actually wound up with.
if (sd4_ != -1) {
struct sockaddr_in sa {};
socklen_t sa_len = sizeof(sa);
if (getsockname(sd4_, reinterpret_cast<sockaddr*>(&sa), &sa_len) == 0) {
port4_ = ntohs(sa.sin_port); // NOLINT
// Aim for a v6 port to match whatever we wound up with on the v4
// side.
port6_ = port4_;
}
}
// Ok now lets try to create an ipv6 socket on the same port.
// (its actually possible to just create a v6 socket and let the OSs
// dual-stack support provide v4 connectivity too, but that's not
// available everywhere (win XP, etc) so let's do this for now.
sd6_ = socket(AF_INET6, SOCK_DGRAM, 0);
if (sd6_ < 0) {
Log("ERROR: Unable to open ipv6 socket: "
+ g_platform->GetSocketErrorString());
} else {
// Since we're explicitly creating both a v4 and v6 socket, tell the v6
// to *not* do both itself (not sure if this is necessary; on mac it
// still seems to come up.. but apparently that's not always the case).
int on = 1;
if (setsockopt(sd6_, IPPROTO_IPV6, IPV6_V6ONLY,
reinterpret_cast<char*>(&on), sizeof(on))
== -1) {
Log("error setting socket as ipv6-only");
}
g_platform->SetSocketNonBlocking(sd6_);
struct sockaddr_in6 serv_addr {};
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin6_family = AF_INET6;
serv_addr.sin6_port = htons(port6_); // NOLINT
serv_addr.sin6_addr = in6addr_any;
result = ::bind(sd6_, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
if (result != 0) {
if (HeadlessMode()) {
Log("FATAL ERROR: unable to bind to requested udp port "
+ std::to_string(port6_) + " (ipv6)");
exit(1);
}
// Primary ipv6 bind failed; try backup.
// We don't care if our random backup ports don't match; only if our
// target port failed.
if (port6_ == initial_requested_port) {
print_port_unavailable = true;
}
serv_addr.sin6_port = htons(0); // NOLINT
result =
::bind(sd6_, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
if (result != 0) {
// Wuh oh; no ipv6 for us i guess.
g_platform->CloseSocket(sd6_);
sd6_ = -1;
}
}
}
// See what v6 port we actually wound up with.
if (sd6_ != -1) {
struct sockaddr_in sa {};
socklen_t sa_len = sizeof(sa);
if (getsockname(sd6_, reinterpret_cast<sockaddr*>(&sa), &sa_len) == 0) {
port6_ = ntohs(sa.sin_port); // NOLINT
}
}
if (print_port_unavailable) {
// FIXME - use translations here
ScreenMessage("Unable to bind udp port "
+ std::to_string(initial_requested_port)
+ "; some network functionality may fail.",
{1, 0.5f, 0});
Log("Unable to bind udp port " + std::to_string(initial_requested_port)
+ "; some network functionality may fail.",
true, false);
}
}
char buffer[10000];
// Now just listen and forward messages along.
while (true) {
struct sockaddr_storage from {};
socklen_t from_size = sizeof(from);
fd_set readset;
FD_ZERO(&readset);
if (sd4_ != -1) {
FD_SET(sd4_, &readset); // NOLINT
}
if (sd6_ != -1) {
FD_SET(sd6_, &readset); // NOLINT
}
int maxfd = std::max(sd4_, sd6_);
int sresult = select(maxfd + 1, &readset, nullptr, nullptr, nullptr);
if (sresult == -1) {
// No big deal if we get interrupted occasionally.
if (g_platform->GetSocketError() == EINTR) {
// Aint no thang.
} else {
// Let's complain for anything else though.
Log("Error on select: " + g_platform->GetSocketErrorString());
}
} else {
int* fds[2] = {&sd4_, &sd6_};
// Wait for any data on either of our sockets.
for (auto& fd : fds) {
int& sd(*fd);
if (sd != -1) {
if (FD_ISSET(sd, &readset)) {
ssize_t rresult =
recvfrom(sd, buffer, sizeof(buffer), 0,
reinterpret_cast<sockaddr*>(&from), &from_size);
if (rresult == 0) {
Log("ERROR: NetworkReader Recv got length 0; this shouldn't "
"happen");
} else if (rresult == -1) {
// This needs to be locked during any sd changes/writes.
std::lock_guard<std::mutex> lock(sd_mutex_);
// If either of our sockets goes down lets close *both* of
// them.
if (sd4_ != -1) {
g_platform->CloseSocket(sd4_);
sd4_ = -1;
}
if (sd6_ != -1) {
g_platform->CloseSocket(sd6_);
sd6_ = -1;
}
} else {
assert(from_size >= 0);
auto rresult2 = static_cast<size_t>(rresult);
// If we get *any* data while paused, kill both our
// sockets (we ping ourself for this purpose).
if (paused_) {
// This needs to be locked during any sd changes/writes.
std::lock_guard<std::mutex> lock(sd_mutex_);
if (sd4_ != -1) {
g_platform->CloseSocket(sd4_);
sd4_ = -1;
}
if (sd6_ != -1) {
g_platform->CloseSocket(sd6_);
sd6_ = -1;
}
break;
}
switch (buffer[0]) {
case BA_PACKET_POKE:
break;
case BA_PACKET_SIMPLE_PING: {
// This needs to be locked during any sd changes/writes.
std::lock_guard<std::mutex> lock(sd_mutex_);
char msg[1] = {BA_PACKET_SIMPLE_PONG};
sendto(sd, msg, 1, 0, reinterpret_cast<sockaddr*>(&from),
from_size);
break;
}
case BA_PACKET_JSON_PING: {
if (rresult2 > 1) {
std::vector<char> s_buffer(rresult2);
memcpy(s_buffer.data(), buffer + 1, rresult2 - 1);
s_buffer[rresult2 - 1] = 0; // terminate string
std::string response = HandleJSONPing(s_buffer.data());
if (!response.empty()) {
std::vector<char> msg(1 + response.size());
msg[0] = BA_PACKET_JSON_PONG;
memcpy(msg.data() + 1, response.c_str(),
response.size());
std::lock_guard<std::mutex> lock(sd_mutex_);
sendto(sd, msg.data(),
static_cast_check_fit<socket_send_length_t>(
msg.size()),
0, reinterpret_cast<sockaddr*>(&from),
from_size);
}
}
break;
}
case BA_PACKET_JSON_PONG: {
if (rresult2 > 1) {
std::vector<char> s_buffer(rresult2);
memcpy(s_buffer.data(), buffer + 1, rresult2 - 1);
s_buffer[rresult2 - 1] = 0; // terminate string
cJSON* data = cJSON_Parse(s_buffer.data());
if (data != nullptr) {
cJSON_Delete(data);
}
}
break;
}
case BA_PACKET_REMOTE_PING:
case BA_PACKET_REMOTE_PONG:
case BA_PACKET_REMOTE_ID_REQUEST:
case BA_PACKET_REMOTE_ID_RESPONSE:
case BA_PACKET_REMOTE_DISCONNECT:
case BA_PACKET_REMOTE_STATE:
case BA_PACKET_REMOTE_STATE2:
case BA_PACKET_REMOTE_STATE_ACK:
case BA_PACKET_REMOTE_DISCONNECT_ACK:
case BA_PACKET_REMOTE_GAME_QUERY:
case BA_PACKET_REMOTE_GAME_RESPONSE:
// These packets are associated with the remote app; let the
// remote server handle them.
if (remote_server_) {
remote_server_->HandleData(
sd, reinterpret_cast<uint8_t*>(buffer), rresult2,
reinterpret_cast<sockaddr*>(&from),
static_cast<size_t>(from_size));
}
break;
case BA_PACKET_CLIENT_REQUEST:
case BA_PACKET_CLIENT_ACCEPT:
case BA_PACKET_CLIENT_DENY:
case BA_PACKET_CLIENT_DENY_ALREADY_IN_PARTY:
case BA_PACKET_CLIENT_DENY_VERSION_MISMATCH:
case BA_PACKET_CLIENT_DENY_PARTY_FULL:
case BA_PACKET_DISCONNECT_FROM_CLIENT_REQUEST:
case BA_PACKET_DISCONNECT_FROM_CLIENT_ACK:
case BA_PACKET_DISCONNECT_FROM_HOST_REQUEST:
case BA_PACKET_DISCONNECT_FROM_HOST_ACK:
case BA_PACKET_CLIENT_GAMEPACKET_COMPRESSED:
case BA_PACKET_HOST_GAMEPACKET_COMPRESSED: {
// These messages are associated with udp host/client
// connections.. pass them to the game thread to wrangle.
std::vector<uint8_t> msg_buffer(rresult2);
memcpy(&(msg_buffer[0]), buffer, rresult2);
g_game->connections()->PushUDPConnectionPacketCall(
msg_buffer, SockAddr(from));
break;
}
case BA_PACKET_GAME_QUERY: {
if (rresult2 == 5) {
// If we're already in a party, don't advertise since they
// wouldn't be able to join us anyway.
if (g_game->connections()->has_connection_to_host()) {
break;
}
// Pull the query id from the packet.
uint32_t query_id;
memcpy(&query_id, buffer + 1, 4);
// Ship them a response packet containing the query id,
// our protocol version, our unique-session-id, and our
// player_spec.
char msg[400];
std::string usid = GetAppInstanceUUID();
std::string player_spec_string;
// If we're signed in, send our account spec.
// Otherwise just send a dummy made with our device name.
player_spec_string =
PlayerSpec::GetAccountPlayerSpec().GetSpecString();
// This should always be the case (len needs to be 1 byte)
assert(player_spec_string.size() < 256);
assert(!usid.empty());
if (usid.size() > 100) {
Log("had to truncate session-id; shouldn't happen");
usid.resize(100);
}
if (usid.empty()) {
usid = "error";
}
msg[0] = BA_PACKET_GAME_QUERY_RESPONSE;
memcpy(msg + 1, &query_id, 4);
uint32_t protocol_version = kProtocolVersion;
memcpy(msg + 5, &protocol_version, 4);
msg[9] = static_cast<char>(usid.size());
msg[10] = static_cast<char>(player_spec_string.size());
memcpy(msg + 11, usid.c_str(), usid.size());
memcpy(msg + 11 + usid.size(), player_spec_string.c_str(),
player_spec_string.size());
size_t msg_len =
11 + player_spec_string.size() + usid.size();
std::vector<uint8_t> msg_buffer(msg_len);
memcpy(&msg_buffer[0], msg, msg_len);
g_network_write_module->PushSendToCall(msg_buffer,
SockAddr(from));
break;
} else {
Log("Error: Got invalid game-query packet of len "
+ std::to_string(rresult2) + "; expected 5.");
}
break;
}
default:
break;
}
}
}
}
}
}
// If *both* of our sockets are dead, break out.
if (sd4_ == -1 && sd6_ == -1) {
break;
}
}
// Sleep for a moment to keep us from running wild if we're unable to block.
Platform::SleepMS(1000);
}
}
NetworkReader::~NetworkReader() = default;
auto NetworkReader::HandleJSONPing(const std::string& data_str) -> std::string {
cJSON* data = cJSON_Parse(data_str.c_str());
if (data == nullptr) {
return "";
}
cJSON_Delete(data);
// Ok lets include some basic info that might be pertinent to someone pinging
// us. Currently that includes our current/max connection count.
char buffer[256];
int party_size = 0;
int party_size_max = 10;
if (g_python != nullptr) {
party_size = g_game->public_party_size();
party_size_max = g_game->public_party_max_size();
}
snprintf(buffer, sizeof(buffer), R"({"b":%d,"ps":%d,"psmx":%d})",
kAppBuildNumber, party_size, party_size_max);
return buffer;
}
} // namespace ballistica

View File

@ -0,0 +1,31 @@
// Released under the MIT License. See LICENSE for details.
#include "ballistica/networking/network_write_module.h"
#include "ballistica/networking/networking.h"
#include "ballistica/networking/sockaddr.h"
namespace ballistica {
NetworkWriteModule::NetworkWriteModule(Thread* thread)
: Module("networkWrite", thread) {
// we're a singleton
assert(g_network_write_module == nullptr);
g_network_write_module = this;
}
void NetworkWriteModule::PushSendToCall(const std::vector<uint8_t>& msg,
const SockAddr& addr) {
// Avoid buffer-full errors if something is causing us to write too often;
// these are unreliable messages so its ok to just drop them.
if (!CheckPushSafety()) {
BA_LOG_ONCE("Excessive send-to calls in net-write-module.");
return;
}
PushCall([this, msg, addr] {
assert(g_network_reader);
Networking::SendTo(msg, addr);
});
}
} // namespace ballistica

View File

@ -0,0 +1,267 @@
// Released under the MIT License. See LICENSE for details.
#include "ballistica/networking/networking.h"
#include "ballistica/app/app_globals.h"
#include "ballistica/game/player_spec.h"
#include "ballistica/networking/network_reader.h"
#include "ballistica/networking/sockaddr.h"
#include "ballistica/platform/platform.h"
namespace ballistica {
struct Networking::ScanResultsEntryPriv {
PlayerSpec player_spec;
std::string address;
uint32_t last_query_id{};
millisecs_t last_contact_time{};
};
Networking::Networking() {
assert(InGameThread());
Resume();
}
Networking::~Networking() = default;
// Note: for now we're making our host-scan network calls directly from the game
// thread. This is generally not a good idea since it appears that even in
// non-blocking mode they're still blocking for 3-4ms sometimes. But for now
// since this is only used minimally and only while in the UI i guess it's ok.
void Networking::HostScanCycle() {
assert(InGameThread());
// We need to create a scanner socket - an ipv4 socket we can send out
// broadcast messages from.
if (scan_socket_ == -1) {
scan_socket_ = socket(AF_INET, SOCK_DGRAM, 0);
if (scan_socket_ == -1) {
Log("Error opening scan socket: " + g_platform->GetSocketErrorString()
+ ".");
return;
}
// Since this guy lives in the game-thread we need it to not block.
if (!g_platform->SetSocketNonBlocking(scan_socket_)) {
Log("Error setting socket non-blocking.");
g_platform->CloseSocket(scan_socket_);
scan_socket_ = -1;
return;
}
// Bind to whatever.
struct sockaddr_in serv_addr {};
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // NOLINT
serv_addr.sin_port = 0; // any
int result =
::bind(scan_socket_, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
if (result == 1) {
Log("Error binding socket: " + g_platform->GetSocketErrorString() + ".");
g_platform->CloseSocket(scan_socket_);
scan_socket_ = -1;
return;
}
// Enable broadcast on the socket.
BA_SOCKET_SETSOCKOPT_VAL_TYPE op_val{1};
result = setsockopt(scan_socket_, SOL_SOCKET, SO_BROADCAST, &op_val,
sizeof(op_val));
if (result != 0) {
Log("Error enabling broadcast for scan-socket: "
+ g_platform->GetSocketErrorString() + ".");
g_platform->CloseSocket(scan_socket_);
scan_socket_ = -1;
return;
}
}
// Ok we've got a valid scanner socket. Now lets send out broadcast pings on
// all available networks.
std::vector<uint32_t> addrs = g_platform->GetBroadcastAddrs();
for (auto&& i : addrs) {
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(kDefaultPort); // NOLINT
addr.sin_addr.s_addr = htonl(i); // NOLINT
// Include our query id (so we can sort out which responses come back
// quickest).
uint8_t data[5];
data[0] = BA_PACKET_GAME_QUERY;
memcpy(data + 1, &next_scan_query_id_, 4);
BA_DEBUG_TIME_CHECK_BEGIN(sendto);
ssize_t result = sendto(
scan_socket_, reinterpret_cast<socket_send_data_t*>(data), sizeof(data),
0, reinterpret_cast<sockaddr*>(&addr), sizeof(addr));
BA_DEBUG_TIME_CHECK_END(sendto, 10);
if (result == -1) {
int err = g_platform->GetSocketError();
switch (err) { // NOLINT(hicpp-multiway-paths-covered)
case ENETUNREACH:
break;
default:
Log("Error on scanSocket sendto: "
+ g_platform->GetSocketErrorString());
}
}
}
next_scan_query_id_++;
// ..and see if any responses came in from previous sends.
char buffer[256];
struct sockaddr_storage from {};
socklen_t from_size = sizeof(from);
while (true) {
BA_DEBUG_TIME_CHECK_BEGIN(recvfrom);
ssize_t result = recvfrom(scan_socket_, buffer, sizeof(buffer), 0,
reinterpret_cast<sockaddr*>(&from), &from_size);
BA_DEBUG_TIME_CHECK_END(recvfrom, 10);
if (result == -1) {
int err = g_platform->GetSocketError();
switch (err) { // NOLINT(hicpp-multiway-paths-covered)
case EWOULDBLOCK:
break;
default:
Log("Error: recvfrom error: " + g_platform->GetSocketErrorString());
break;
}
break;
}
if (result > 2 && buffer[0] == BA_PACKET_GAME_QUERY_RESPONSE) {
// Size should be between 13 and 366 (1 byte type, 4 byte query_id, 4
// byte protocol_id, 1 byte id_len, 1 byte player_spec_len, 1-100 byte
// id, 1-255 byte player-spec).
if (result >= 14 && result <= 366) {
uint32_t protocol_version;
uint32_t query_id;
memcpy(&query_id, buffer + 1, 4);
memcpy(&protocol_version, buffer + 5, 4);
auto id_len = static_cast<uint32_t>(buffer[9]);
auto player_spec_len = static_cast<uint32_t>(buffer[10]);
if (id_len > 0 && id_len <= 100 && player_spec_len > 0
&& player_spec_len <= 255
&& (11 + id_len + player_spec_len == result)) {
char id[101];
char player_spec_str[256];
memcpy(id, buffer + 11, id_len);
memcpy(player_spec_str, buffer + 11 + id_len, player_spec_len);
id[id_len] = 0;
player_spec_str[player_spec_len] = 0;
// Add or modify an entry for this.
{
std::lock_guard<std::mutex> lock(scan_results_mutex_);
// Ignore if it looks like its us.
if (id != GetAppInstanceUUID()) {
std::string key = id;
auto i = scan_results_.find(key);
// Make a new entry if its not there.
bool do_update_entry = (i == scan_results_.end()
|| i->second.last_query_id != query_id);
if (do_update_entry) {
ScanResultsEntryPriv& entry(scan_results_[key]);
entry.player_spec = PlayerSpec(player_spec_str);
char buffer2[256];
entry.address = inet_ntop(
AF_INET,
&((reinterpret_cast<sockaddr_in*>(&from))->sin_addr),
buffer2, sizeof(buffer2));
entry.last_query_id = query_id;
entry.last_contact_time = GetRealTime();
}
}
PruneScanResults();
}
} else {
Log("Error: Got invalid BA_PACKET_GAME_QUERY_RESPONSE packet");
}
} else {
Log("Error: Got invalid BA_PACKET_GAME_QUERY_RESPONSE packet");
}
}
}
}
auto Networking::GetScanResults() -> std::vector<Networking::ScanResultsEntry> {
std::vector<ScanResultsEntry> results;
results.resize(scan_results_.size());
{
std::lock_guard<std::mutex> lock(scan_results_mutex_);
int out_num = 0;
for (auto&& i : scan_results_) {
ScanResultsEntryPriv& in(i.second);
ScanResultsEntry& out(results[out_num]);
out.display_string = in.player_spec.GetDisplayString();
out.address = in.address;
out_num++;
}
PruneScanResults();
}
return results;
}
void Networking::PruneScanResults() {
millisecs_t t = GetRealTime();
auto i = scan_results_.begin();
while (i != scan_results_.end()) {
auto i_next = i;
i_next++;
if (t - i->second.last_contact_time > 3000) {
scan_results_.erase(i);
}
i = i_next;
}
}
void Networking::EndHostScanning() {
if (scan_socket_ != -1) {
g_platform->CloseSocket(scan_socket_);
scan_socket_ = -1;
}
}
void Networking::Pause() {
if (!running_) Log("Networking::pause() called with running_ already false");
running_ = false;
// Game is going into background or whatnot. Kill any sockets/etc.
EndHostScanning();
}
void Networking::Resume() {
if (running_) {
Log("Networking::resume() called with running_ already true");
}
running_ = true;
}
void Networking::SendTo(const std::vector<uint8_t>& buffer,
const SockAddr& addr) {
assert(g_network_reader);
assert(!buffer.empty());
// This needs to be locked during any sd changes/writes.
std::lock_guard<std::mutex> lock(g_network_reader->sd_mutex());
// Only send if the relevant socket is currently up.. silently ignore
// otherwise.
int sd = addr.IsV6() ? g_network_reader->sd6() : g_network_reader->sd4();
if (sd != -1) {
sendto(sd, (const char*)&buffer[0],
static_cast_check_fit<socket_send_length_t>(buffer.size()), 0,
addr.GetSockAddr(), addr.GetSockAddrLen());
}
}
} // namespace ballistica

View File

@ -7,28 +7,8 @@ namespace ballistica {
SockAddr::SockAddr(const std::string& addr, int port) {
memset(&addr_, 0, sizeof(addr_));
// try ipv4...
// Try ipv4 and then ipv6.
{
// inet_pton is not available on XP :-/
// hmmm at this point we probably don't care; should test inet_pton.
// #if BA_OSTYPE_WINDOWS
// int addr_size = sizeof(addr_);
// std::wstring addr2;
// addr2.assign(addr.begin(), addr.end());
// struct sockaddr_in* a4 = reinterpret_cast<sockaddr_in*>(&addr_);
// struct sockaddr_in6* a6 = reinterpret_cast<sockaddr_in6*>(&addr_);
// int result =
// WSAStringToAddress(const_cast<wchar_t*>(addr2.c_str()), AF_INET,
// nullptr, (LPSOCKADDR)a4, &addr_size);
// if (result == 0) {
// if (a4->sin_family == AF_INET) {
// a4->sin_port = htons(port);
// return;
// } else if (a6->sin6_family == AF_INET6) {
// a6->sin6_port = htons(port);
// }
// }
// #else
struct in_addr addr_out {};
int result = inet_pton(AF_INET, addr.c_str(), &addr_out);
if (result == 1) {
@ -48,9 +28,8 @@ SockAddr::SockAddr(const std::string& addr, int port) {
return;
}
}
// #endif
}
throw Exception("Invalid address: '" + addr + "'");
throw Exception("Invalid address: '" + addr + "'.");
}
} // namespace ballistica

View File

@ -0,0 +1,558 @@
// Released under the MIT License. See LICENSE for details.
#include "ballistica/python/methods/python_methods_networking.h"
#include "ballistica/app/app_globals.h"
#include "ballistica/game/connection/connection_set.h"
#include "ballistica/game/connection/connection_to_client.h"
#include "ballistica/game/connection/connection_to_host.h"
#include "ballistica/game/game.h"
#include "ballistica/math/vector3f.h"
#include "ballistica/networking/network_reader.h"
#include "ballistica/networking/networking.h"
#include "ballistica/networking/sockaddr.h"
#include "ballistica/networking/telnet_server.h"
#include "ballistica/platform/platform.h"
#include "ballistica/python/python.h"
#include "ballistica/python/python_sys.h"
namespace ballistica {
// Ignore signed bitwise stuff; python macros do it quite a bit.
#pragma clang diagnostic push
#pragma ide diagnostic ignored "hicpp-signed-bitwise"
auto PyGetPublicPartyEnabled(PyObject* self, PyObject* args, PyObject* keywds)
-> PyObject* {
BA_PYTHON_TRY;
Platform::SetLastPyCall("getpublicpartyenabled");
static const char* kwlist[] = {nullptr};
if (!PyArg_ParseTupleAndKeywords(args, keywds, "",
const_cast<char**>(kwlist)))
return nullptr;
assert(g_python);
if (g_game->public_party_enabled()) {
Py_RETURN_TRUE;
} else {
Py_RETURN_FALSE;
}
BA_PYTHON_CATCH;
}
auto PySetPublicPartyEnabled(PyObject* self, PyObject* args, PyObject* keywds)
-> PyObject* {
BA_PYTHON_TRY;
Platform::SetLastPyCall("setpublicpartyenabled");
int enable;
static const char* kwlist[] = {"enabled", nullptr};
if (!PyArg_ParseTupleAndKeywords(args, keywds, "p",
const_cast<char**>(kwlist), &enable)) {
return nullptr;
}
assert(g_python);
g_game->SetPublicPartyEnabled(static_cast<bool>(enable));
Py_RETURN_NONE;
BA_PYTHON_CATCH;
}
auto PySetPublicPartyName(PyObject* self, PyObject* args, PyObject* keywds)
-> PyObject* {
BA_PYTHON_TRY;
Platform::SetLastPyCall("setpublicpartyname");
PyObject* name_obj;
static const char* kwlist[] = {"name", nullptr};
if (!PyArg_ParseTupleAndKeywords(args, keywds, "O",
const_cast<char**>(kwlist), &name_obj)) {
return nullptr;
}
std::string name = Python::GetPyString(name_obj);
assert(g_python);
g_game->SetPublicPartyName(name);
Py_RETURN_NONE;
BA_PYTHON_CATCH;
}
auto PySetPublicPartyStatsURL(PyObject* self, PyObject* args, PyObject* keywds)
-> PyObject* {
BA_PYTHON_TRY;
Platform::SetLastPyCall("setpublicpartystatsurl");
PyObject* url_obj;
static const char* kwlist[] = {"url", nullptr};
if (!PyArg_ParseTupleAndKeywords(args, keywds, "O",
const_cast<char**>(kwlist), &url_obj)) {
return nullptr;
}
// The call expects an empty string for the no-url option.
std::string url = (url_obj == Py_None) ? "" : Python::GetPyString(url_obj);
assert(g_python);
g_game->SetPublicPartyStatsURL(url);
Py_RETURN_NONE;
BA_PYTHON_CATCH;
}
auto PyGetPublicPartyMaxSize(PyObject* self, PyObject* args, PyObject* keywds)
-> PyObject* {
BA_PYTHON_TRY;
Platform::SetLastPyCall("getpublicpartymaxsize");
static const char* kwlist[] = {nullptr};
if (!PyArg_ParseTupleAndKeywords(args, keywds, "",
const_cast<char**>(kwlist))) {
return nullptr;
}
assert(g_python);
return PyLong_FromLong(g_game->public_party_max_size());
BA_PYTHON_CATCH;
}
auto PySetPublicPartyMaxSize(PyObject* self, PyObject* args, PyObject* keywds)
-> PyObject* {
BA_PYTHON_TRY;
Platform::SetLastPyCall("setpublicpartymaxsize");
int max_size;
static const char* kwlist[] = {"max_size", nullptr};
if (!PyArg_ParseTupleAndKeywords(args, keywds, "i",
const_cast<char**>(kwlist), &max_size)) {
return nullptr;
}
assert(g_python);
g_game->SetPublicPartyMaxSize(max_size);
Py_RETURN_NONE;
BA_PYTHON_CATCH;
}
auto PySetAuthenticateClients(PyObject* self, PyObject* args, PyObject* keywds)
-> PyObject* {
BA_PYTHON_TRY;
Platform::SetLastPyCall("set_authenticate_clients");
int enable;
static const char* kwlist[] = {"enable", nullptr};
if (!PyArg_ParseTupleAndKeywords(args, keywds, "p",
const_cast<char**>(kwlist), &enable)) {
return nullptr;
}
assert(g_game);
g_game->set_require_client_authentication(static_cast<bool>(enable));
Py_RETURN_NONE;
BA_PYTHON_CATCH;
}
auto PySetAdmins(PyObject* self, PyObject* args, PyObject* keywds)
-> PyObject* {
BA_PYTHON_TRY;
Platform::SetLastPyCall("set_admins");
PyObject* admins_obj;
static const char* kwlist[] = {"admins", nullptr};
if (!PyArg_ParseTupleAndKeywords(args, keywds, "O",
const_cast<char**>(kwlist), &admins_obj)) {
return nullptr;
}
assert(g_game);
auto admins = Python::GetPyStrings(admins_obj);
std::set<std::string> adminset;
for (auto&& admin : admins) {
adminset.insert(admin);
}
g_game->set_admin_public_ids(adminset);
Py_RETURN_NONE;
BA_PYTHON_CATCH;
}
auto PySetEnableDefaultKickVoting(PyObject* self, PyObject* args,
PyObject* keywds) -> PyObject* {
BA_PYTHON_TRY;
Platform::SetLastPyCall("set_enable_default_kick_voting");
int enable;
static const char* kwlist[] = {"enable", nullptr};
if (!PyArg_ParseTupleAndKeywords(args, keywds, "p",
const_cast<char**>(kwlist), &enable)) {
return nullptr;
}
assert(g_game);
g_game->set_kick_voting_enabled(static_cast<bool>(enable));
Py_RETURN_NONE;
BA_PYTHON_CATCH;
}
auto PyConnectToParty(PyObject* self, PyObject* args, PyObject* keywds)
-> PyObject* {
BA_PYTHON_TRY;
Platform::SetLastPyCall("connect_to_party");
std::string address;
PyObject* address_obj;
int port = kDefaultPort;
// Whether we should print standard 'connecting...' and 'party full..'
// messages when false, only odd errors such as version incompatibility will
// be printed and most connection attempts will be silent todo: could
// generalize this to pass all results to a callback instead
int print_progress = 1;
static const char* kwlist[] = {"address", "port", "print_progress", nullptr};
if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|ip",
const_cast<char**>(kwlist), &address_obj,
&port, &print_progress)) {
return nullptr;
}
address = Python::GetPyString(address_obj);
// Disallow in headless build (people were using this for spam-bots).
if (HeadlessMode()) {
throw Exception("Not available in headless mode.");
}
SockAddr s;
try {
s = SockAddr(address, port);
// HACK: CLion currently flags our catch clause as unreachable even
// though SockAddr constructor can throw exceptions. Work around that here.
if (explicit_bool(false)) {
throw Exception();
}
} catch (const std::exception&) {
ScreenMessage(g_game->GetResourceString("invalidAddressErrorText"),
{1, 0, 0});
Py_RETURN_NONE;
}
g_game->connections()->PushHostConnectedUDPCall(
s, static_cast<bool>(print_progress));
Py_RETURN_NONE;
BA_PYTHON_CATCH;
}
auto PyClientInfoQueryResponse(PyObject* self, PyObject* args, PyObject* keywds)
-> PyObject* {
BA_PYTHON_TRY;
Platform::SetLastPyCall("client_info_query_response");
const char* token;
PyObject* response_obj;
static const char* kwlist[] = {"token", "response", nullptr};
if (!PyArg_ParseTupleAndKeywords(args, keywds, "sO",
const_cast<char**>(kwlist), &token,
&response_obj)) {
return nullptr;
}
g_game->connections()->SetClientInfoFromMasterServer(token, response_obj);
Py_RETURN_NONE;
BA_PYTHON_CATCH;
}
auto PyGetConnectionToHostInfo(PyObject* self, PyObject* args, PyObject* keywds)
-> PyObject* {
BA_PYTHON_TRY;
Platform::SetLastPyCall("get_connection_to_host_info");
static const char* kwlist[] = {nullptr};
if (!PyArg_ParseTupleAndKeywords(args, keywds, "",
const_cast<char**>(kwlist))) {
return nullptr;
}
ConnectionToHost* hc = g_game->connections()->connection_to_host();
if (hc) {
return Py_BuildValue("{sssi}", "name", hc->party_name().c_str(),
"build_number", hc->build_number());
} else {
return Py_BuildValue("{}");
}
Py_RETURN_NONE;
BA_PYTHON_CATCH;
}
auto PyDisconnectFromHost(PyObject* self, PyObject* args, PyObject* keywds)
-> PyObject* {
BA_PYTHON_TRY;
Platform::SetLastPyCall("disconnect_from_host");
static const char* kwlist[] = {nullptr};
if (!PyArg_ParseTupleAndKeywords(args, keywds, "",
const_cast<char**>(kwlist))) {
return nullptr;
}
g_game->connections()->PushDisconnectFromHostCall();
Py_RETURN_NONE;
BA_PYTHON_CATCH;
}
auto PyDisconnectClient(PyObject* self, PyObject* args, PyObject* keywds)
-> PyObject* {
BA_PYTHON_TRY;
Platform::SetLastPyCall("disconnect_client");
int client_id;
int ban_time = 300; // Old default before we exposed this.
static const char* kwlist[] = {"client_id", "ban_time", nullptr};
if (!PyArg_ParseTupleAndKeywords(args, keywds, "i|i",
const_cast<char**>(kwlist), &client_id,
&ban_time)) {
return nullptr;
}
bool kickable = g_game->connections()->DisconnectClient(client_id, ban_time);
if (kickable) {
Py_RETURN_TRUE;
} else {
Py_RETURN_FALSE;
}
BA_PYTHON_CATCH;
}
auto PyGetClientPublicDeviceUUID(PyObject* self, PyObject* args,
PyObject* keywds) -> PyObject* {
BA_PYTHON_TRY;
Platform::SetLastPyCall("get_client_public_device_uuid");
int client_id;
static const char* kwlist[] = {"client_id", nullptr};
if (!PyArg_ParseTupleAndKeywords(args, keywds, "i",
const_cast<char**>(kwlist), &client_id)) {
return nullptr;
}
auto&& connection{
g_game->connections()->connections_to_clients().find(client_id)};
// Does this connection exist?
if (connection == g_game->connections()->connections_to_clients().end()) {
Py_RETURN_NONE;
}
// Connections should always be valid refs.
assert(connection->second.exists());
// Old clients don't assign this; it will be empty.
if (connection->second->public_device_id().empty()) {
Py_RETURN_NONE;
}
return PyUnicode_FromString(connection->second->public_device_id().c_str());
BA_PYTHON_CATCH;
}
auto PyGetGamePort(PyObject* self, PyObject* args) -> PyObject* {
BA_PYTHON_TRY;
Platform::SetLastPyCall("get_game_port");
int port = 0;
if (g_network_reader != nullptr) {
// Hmmm; we're just fetching the ipv4 port here; 6 could be different.
port = g_network_reader->port4();
}
return Py_BuildValue("i", port);
BA_PYTHON_CATCH;
}
auto PySetMasterServerSource(PyObject* self, PyObject* args) -> PyObject* {
BA_PYTHON_TRY;
Platform::SetLastPyCall("set_master_server_source");
int source;
if (!PyArg_ParseTuple(args, "i", &source)) return nullptr;
if (source != 0 && source != 1) {
BA_LOG_ONCE("Error: Invalid server source: " + std::to_string(source)
+ ".");
source = 1;
}
g_app_globals->master_server_source = source;
Py_RETURN_NONE;
BA_PYTHON_CATCH;
}
auto PySetTelnetAccessEnabled(PyObject* self, PyObject* args, PyObject* keywds)
-> PyObject* {
BA_PYTHON_TRY;
Platform::SetLastPyCall("set_telnet_access_enabled");
assert(InGameThread());
int enable;
static const char* kwlist[] = {"enable", nullptr};
if (!PyArg_ParseTupleAndKeywords(args, keywds, "p",
const_cast<char**>(kwlist), &enable)) {
return nullptr;
}
if (g_app_globals->telnet_server) {
g_app_globals->telnet_server->SetAccessEnabled(static_cast<bool>(enable));
} else {
throw Exception("Telnet server not enabled.");
}
Py_RETURN_NONE;
BA_PYTHON_CATCH;
}
auto PyHostScanCycle(PyObject* self, PyObject* args, PyObject* keywds)
-> PyObject* {
BA_PYTHON_TRY;
Platform::SetLastPyCall("host_scan_cycle");
g_networking->HostScanCycle();
std::vector<Networking::ScanResultsEntry> results =
g_networking->GetScanResults();
PyObject* py_list = PyList_New(0);
for (auto&& i : results) {
PyList_Append(py_list, Py_BuildValue("{ssss}", "display_string",
i.display_string.c_str(), "address",
i.address.c_str()));
}
return py_list;
BA_PYTHON_CATCH;
}
auto PyEndHostScanning(PyObject* self, PyObject* args, PyObject* keywds)
-> PyObject* {
BA_PYTHON_TRY;
Platform::SetLastPyCall("end_host_scanning");
g_networking->EndHostScanning();
Py_RETURN_NONE;
BA_PYTHON_CATCH;
}
auto PyHaveConnectedClients(PyObject* self, PyObject* args, PyObject* keywds)
-> PyObject* {
BA_PYTHON_TRY;
Platform::SetLastPyCall("have_connected_clients");
if (g_game->connections()->GetConnectedClientCount() > 0) {
Py_RETURN_TRUE;
} else {
Py_RETURN_FALSE;
}
BA_PYTHON_CATCH;
}
auto PythonMethodsNetworking::GetMethods() -> std::vector<PyMethodDef> {
return {
{"have_connected_clients", (PyCFunction)PyHaveConnectedClients,
METH_VARARGS | METH_KEYWORDS,
"have_connected_clients() -> bool\n"
"\n"
"(internal)\n"
"\n"
"Category: General Utility Functions"},
{"end_host_scanning", (PyCFunction)PyEndHostScanning,
METH_VARARGS | METH_KEYWORDS,
"end_host_scanning() -> None\n"
"\n"
"(internal)\n"
"\n"
"Category: General Utility Functions"},
{"host_scan_cycle", (PyCFunction)PyHostScanCycle,
METH_VARARGS | METH_KEYWORDS,
"host_scan_cycle() -> list\n"
"\n"
"(internal)"},
{"set_telnet_access_enabled", (PyCFunction)PySetTelnetAccessEnabled,
METH_VARARGS | METH_KEYWORDS,
"set_telnet_access_enabled(enable: bool)\n"
" -> None\n"
"\n"
"(internal)"},
{"set_master_server_source", PySetMasterServerSource, METH_VARARGS,
"set_master_server_source(source: int) -> None\n"
"\n"
"(internal)"},
{"get_game_port", PyGetGamePort, METH_VARARGS,
"get_game_port() -> int\n"
"\n"
"(internal)\n"
"\n"
"Return the port ballistica is hosting on."},
{"disconnect_from_host", (PyCFunction)PyDisconnectFromHost,
METH_VARARGS | METH_KEYWORDS,
"disconnect_from_host() -> None\n"
"\n"
"(internal)\n"
"\n"
"Category: General Utility Functions"},
{"disconnect_client", (PyCFunction)PyDisconnectClient,
METH_VARARGS | METH_KEYWORDS,
"disconnect_client(client_id: int, ban_time: int = 300) -> bool\n"
"\n"
"(internal)"},
{"get_client_public_device_uuid",
(PyCFunction)PyGetClientPublicDeviceUUID, METH_VARARGS | METH_KEYWORDS,
"get_client_public_device_uuid(client_id: int) -> Optional[str]\n"
"\n"
"(internal)\n"
"\n"
"Category: General Utility Functions\n"
"\n"
"Return a public device UUID for a client. If the client does not\n"
"exist or is running a version older than 1.6.10, returns None.\n"
"Public device UUID uniquely identifies the device the client is\n"
"using in a semi-permanent way. The UUID value will change\n"
"periodically with updates to the game or operating system."},
{"get_connection_to_host_info", (PyCFunction)PyGetConnectionToHostInfo,
METH_VARARGS | METH_KEYWORDS,
"get_connection_to_host_info() -> dict\n"
"\n"
"(internal)"},
{"client_info_query_response", (PyCFunction)PyClientInfoQueryResponse,
METH_VARARGS | METH_KEYWORDS,
"client_info_query_response(token: str, response: Any) -> None\n"
"\n"
"(internal)"},
{"connect_to_party", (PyCFunction)PyConnectToParty,
METH_VARARGS | METH_KEYWORDS,
"connect_to_party(address: str, port: int = None,\n"
" print_progress: bool = True) -> None\n"
"\n"
"(internal)"},
{"set_authenticate_clients", (PyCFunction)PySetAuthenticateClients,
METH_VARARGS | METH_KEYWORDS,
"set_authenticate_clients(enable: bool) -> None\n"
"\n"
"(internal)"},
{"set_admins", (PyCFunction)PySetAdmins, METH_VARARGS | METH_KEYWORDS,
"set_admins(admins: list[str]) -> None\n"
"\n"
"(internal)"},
{"set_enable_default_kick_voting",
(PyCFunction)PySetEnableDefaultKickVoting, METH_VARARGS | METH_KEYWORDS,
"set_enable_default_kick_voting(enable: bool) -> None\n"
"\n"
"(internal)"},
{"set_public_party_max_size", (PyCFunction)PySetPublicPartyMaxSize,
METH_VARARGS | METH_KEYWORDS,
"set_public_party_max_size(max_size: int) -> None\n"
"\n"
"(internal)"},
{"get_public_party_max_size", (PyCFunction)PyGetPublicPartyMaxSize,
METH_VARARGS | METH_KEYWORDS,
"get_public_party_max_size() -> int\n"
"\n"
"(internal)"},
{"set_public_party_stats_url", (PyCFunction)PySetPublicPartyStatsURL,
METH_VARARGS | METH_KEYWORDS,
"set_public_party_stats_url(url: Optional[str]) -> None\n"
"\n"
"(internal)"},
{"set_public_party_name", (PyCFunction)PySetPublicPartyName,
METH_VARARGS | METH_KEYWORDS,
"set_public_party_name(name: str) -> None\n"
"\n"
"(internal)"},
{"set_public_party_enabled", (PyCFunction)PySetPublicPartyEnabled,
METH_VARARGS | METH_KEYWORDS,
"set_public_party_enabled(enabled: bool) -> None\n"
"\n"
"(internal)"},
{"get_public_party_enabled", (PyCFunction)PyGetPublicPartyEnabled,
METH_VARARGS | METH_KEYWORDS,
"get_public_party_enabled() -> bool\n"
"\n"
"(internal)"},
};
}
#pragma clang diagnostic pop
} // namespace ballistica

View File

@ -0,0 +1,20 @@
// Released under the MIT License. See LICENSE for details.
#ifndef BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_NETWORKING_H_
#define BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_NETWORKING_H_
#include <vector>
#include "ballistica/ballistica.h"
namespace ballistica {
/// Networking related individual python methods for our module.
class PythonMethodsNetworking {
public:
static auto GetMethods() -> std::vector<PyMethodDef>;
};
} // namespace ballistica
#endif // BALLISTICA_PYTHON_METHODS_PYTHON_METHODS_NETWORKING_H_

View File

@ -39,6 +39,7 @@
#include "ballistica/python/methods/python_methods_graphics.h"
#include "ballistica/python/methods/python_methods_input.h"
#include "ballistica/python/methods/python_methods_media.h"
#include "ballistica/python/methods/python_methods_networking.h"
#include "ballistica/python/methods/python_methods_system.h"
#include "ballistica/python/methods/python_methods_ui.h"
#include "ballistica/python/python_command.h"
@ -1005,6 +1006,7 @@ void Python::Reset(bool do_init) {
auto Python::GetModuleMethods() -> std::vector<PyMethodDef> {
std::vector<PyMethodDef> all_methods;
for (auto&& methods : {
PythonMethodsNetworking::GetMethods(),
PythonMethodsUI::GetMethods(),
PythonMethodsInput::GetMethods(),
PythonMethodsApp::GetMethods(),