mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-27 17:33:13 +08:00
Exposing networking sources
This commit is contained in:
parent
8ddaf71bf8
commit
f0e1d7bdde
930
.efrocachemap
930
.efrocachemap
@ -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"
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -1 +1 @@
|
||||
14398100813069830297938811166218395775
|
||||
25124287646962522356366356681900113695
|
||||
@ -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
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -95,6 +95,7 @@ class Input;
|
||||
class InputDevice;
|
||||
struct JointFixedEF;
|
||||
class Joystick;
|
||||
class JsonDict;
|
||||
class KeyboardInput;
|
||||
class Material;
|
||||
class MaterialAction;
|
||||
|
||||
495
src/ballistica/game/connection/connection.cc
Normal file
495
src/ballistica/game/connection/connection.cc
Normal 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
|
||||
707
src/ballistica/game/connection/connection_set.cc
Normal file
707
src/ballistica/game/connection/connection_set.cc
Normal 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
|
||||
762
src/ballistica/game/connection/connection_to_client.cc
Normal file
762
src/ballistica/game/connection/connection_to_client.cc
Normal 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
|
||||
95
src/ballistica/game/connection/connection_to_client_udp.cc
Normal file
95
src/ballistica/game/connection/connection_to_client_udp.cc
Normal 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
|
||||
494
src/ballistica/game/connection/connection_to_host.cc
Normal file
494
src/ballistica/game/connection/connection_to_host.cc
Normal 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
|
||||
185
src/ballistica/game/connection/connection_to_host_udp.cc
Normal file
185
src/ballistica/game/connection/connection_to_host_udp.cc
Normal 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
|
||||
493
src/ballistica/networking/network_reader.cc
Normal file
493
src/ballistica/networking/network_reader.cc
Normal 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
|
||||
31
src/ballistica/networking/network_write_module.cc
Normal file
31
src/ballistica/networking/network_write_module.cc
Normal 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
|
||||
267
src/ballistica/networking/networking.cc
Normal file
267
src/ballistica/networking/networking.cc
Normal 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
|
||||
@ -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
|
||||
|
||||
558
src/ballistica/python/methods/python_methods_networking.cc
Normal file
558
src/ballistica/python/methods/python_methods_networking.cc
Normal 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
|
||||
20
src/ballistica/python/methods/python_methods_networking.h
Normal file
20
src/ballistica/python/methods/python_methods_networking.h
Normal 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_
|
||||
@ -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(),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user