From 98d9a8dbe0e4bfebcec877bcf97eb3a1f0b4a684 Mon Sep 17 00:00:00 2001 From: Ryan Shavell Date: Sun, 6 Dec 2020 21:19:58 -0500 Subject: [PATCH 01/88] Living impostors can faintly hear nearby ghosts --- src/renderer/Voice.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index 8c2a874e..9ff74d33 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -71,17 +71,20 @@ function calculateVoiceAudio(state: AmongUsState, settings: ISettings, me: Playe if (isNaN(panPos[1])) panPos[1] = 999; panPos[0] = Math.min(999, Math.max(-999, panPos[0])); panPos[1] = Math.min(999, Math.max(-999, panPos[1])); + // Don't hear people inside vents if (other.inVent) { gain.gain.value = 0; return; } + // Ghosts can hear other ghosts if (me.isDead && other.isDead) { gain.gain.value = 1; pan.positionX.setValueAtTime(panPos[0], audioContext.currentTime); pan.positionY.setValueAtTime(panPos[1], audioContext.currentTime); return; } - if (!me.isDead && other.isDead) { + // Living crewmates cannot hear ghosts + if (!me.isDead && other.isDead && !me.isImpostor) { gain.gain.value = 0; return; } @@ -101,6 +104,10 @@ function calculateVoiceAudio(state: AmongUsState, settings: ISettings, me: Playe if (gain.gain.value === 1 && Math.sqrt(Math.pow(panPos[0], 2) + Math.pow(panPos[1], 2)) > 7) { gain.gain.value = 0; } + // Living impostors hear ghosts at a faint volume + if (gain.gain.value > 0 && !me.isDead && me.isImpostor && other.isDead) { + gain.gain.value = gain.gain.value * 0.015; + } } From ff3d1216305daf626ae0ddb499b6cbe38578b2a3 Mon Sep 17 00:00:00 2001 From: Ryan Shavell Date: Sun, 6 Dec 2020 22:59:27 -0500 Subject: [PATCH 02/88] Add reverb to ghosts talking to impostors, add haunting setting --- src/renderer/App.tsx | 3 +- src/renderer/Settings.tsx | 12 ++++++++ src/renderer/Voice.tsx | 59 +++++++++++++++++++++++++++++++++------ 3 files changed, 64 insertions(+), 10 deletions(-) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index c572eee9..3ea80167 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -39,7 +39,8 @@ function App() { data: '' }, hideCode: false, - stereoInLobby: true + stereoInLobby: true, + haunting: true }); useEffect(() => { diff --git a/src/renderer/Settings.tsx b/src/renderer/Settings.tsx index 379ef587..bc08f4df 100644 --- a/src/renderer/Settings.tsx +++ b/src/renderer/Settings.tsx @@ -83,6 +83,10 @@ const store = new Store({ stereoInLobby: { type: 'boolean', default: true + }, + haunting: { + type: 'boolean', + default: true } } }); @@ -106,6 +110,7 @@ export interface ISettings { }, hideCode: boolean; stereoInLobby: boolean; + haunting: boolean; } export const settingsReducer = (state: ISettings, action: { type: 'set' | 'setOne', action: [string, any] | ISettings @@ -314,6 +319,13 @@ export default function Settings({ open, onClose }: SettingsProps) { +
setSettings({ + type: 'setOne', + action: ['haunting', !settings.haunting] + })}> + + +
} \ No newline at end of file diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index 9ff74d33..a5c638ef 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -19,6 +19,9 @@ interface AudioElements { element: HTMLAudioElement; gain: GainNode; pan: PannerNode; + reverbGain: GainNode; + reverb: ConvolverNode; + compressor: DynamicsCompressorNode; }; } interface AudioListeners { @@ -44,6 +47,8 @@ interface OtherDead { [playerId: number]: boolean; // isTalking } +var impulseResponse = "T2dnUwACAAAAAAAAAADAewAAAAAAALxEBMUBHgF2b3JiaXMAAAAAAkSsAAAAAAAAAHECAAAAAAC4AU9nZ1MAAAAAAAAAAAAAwHsAAAEAAADuq7S9ElT/////////////////////kQN2b3JiaXMrAAAAWGlwaC5PcmcgbGliVm9yYmlzIEkgMjAxMjAyMDMgKE9tbmlwcmVzZW50KQEAAAAVAAAAQVJUSVNUPURvbkthcmxzc29uU2FuAQV2b3JiaXMpQkNWAQAIAAAAMUwgxYDQkFUAABAAAGAkKQ6TZkkppZShKHmYlEhJKaWUxTCJmJSJxRhjjDHGGGOMMcYYY4wgNGQVAAAEAIAoCY6j5klqzjlnGCeOcqA5aU44pyAHilHgOQnC9SZjbqa0pmtuziklCA1ZBQAAAgBASCGFFFJIIYUUYoghhhhiiCGHHHLIIaeccgoqqKCCCjLIIINMMumkk0466aijjjrqKLTQQgsttNJKTDHVVmOuvQZdfHPOOeecc84555xzzglCQ1YBACAAAARCBhlkEEIIIYUUUogppphyCjLIgNCQVQAAIACAAAAAAEeRFEmxFMuxHM3RJE/yLFETNdEzRVNUTVVVVVV1XVd2Zdd2ddd2fVmYhVu4fVm4hVvYhV33hWEYhmEYhmEYhmH4fd/3fd/3fSA0ZBUAIAEAoCM5luMpoiIaouI5ogOEhqwCAGQAAAQAIAmSIimSo0mmZmquaZu2aKu2bcuyLMuyDISGrAIAAAEABAAAAAAAoGmapmmapmmapmmapmmapmmapmmaZlmWZVmWZVmWZVmWZVmWZVmWZVmWZVmWZVmWZVmWZVmWZVmWZVlAaMgqAEACAEDHcRzHcSRFUiTHciwHCA1ZBQDIAAAIAEBSLMVyNEdzNMdzPMdzPEd0RMmUTM30TA8IDVkFAAACAAgAAAAAAEAxHMVxHMnRJE9SLdNyNVdzPddzTdd1XVdVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVgdCQVQAABAAAIZ1mlmqACDOQYSA0ZBUAgAAAABihCEMMCA1ZBQAABAAAiKHkIJrQmvPNOQ6a5aCpFJvTwYlUmye5qZibc84555xszhnjnHPOKcqZxaCZ0JpzzkkMmqWgmdCac855EpsHranSmnPOGeecDsYZYZxzzmnSmgep2Vibc85Z0JrmqLkUm3POiZSbJ7W5VJtzzjnnnHPOOeecc86pXpzOwTnhnHPOidqba7kJXZxzzvlknO7NCeGcc84555xzzjnnnHPOCUJDVgEAQAAABGHYGMadgiB9jgZiFCGmIZMedI8Ok6AxyCmkHo2ORkqpg1BSGSeldILQkFUAACAAAIQQUkghhRRSSCGFFFJIIYYYYoghp5xyCiqopJKKKsoos8wyyyyzzDLLrMPOOuuwwxBDDDG00kosNdVWY4215p5zrjlIa6W11lorpZRSSimlIDRkFQAAAgBAIGSQQQYZhRRSSCGGmHLKKaegggoIDVkFAAACAAgAAADwJM8RHdERHdERHdERHdERHc/xHFESJVESJdEyLVMzPVVUVVd2bVmXddu3hV3Ydd/Xfd/XjV8XhmVZlmVZlmVZlmVZlmVZlmUJQkNWAQAgAAAAQgghhBRSSCGFlGKMMcecg05CCYHQkFUAACAAgAAAAABHcRTHkRzJkSRLsiRN0izN8jRP8zTRE0VRNE1TFV3RFXXTFmVTNl3TNWXTVWXVdmXZtmVbt31Ztn3f933f933f933f933f13UgNGQVACABAKAjOZIiKZIiOY7jSJIEhIasAgBkAAAEAKAojuI4jiNJkiRZkiZ5lmeJmqmZnumpogqEhqwCAAABAAQAAAAAAKBoiqeYiqeIiueIjiiJlmmJmqq5omzKruu6ruu6ruu6ruu6ruu6ruu6ruu6ruu6ruu6ruu6ruu6ruu6QGjIKgBAAgBAR3IkR3IkRVIkRXIkBwgNWQUAyAAACADAMRxDUiTHsixN8zRP8zTREz3RMz1VdEUXCA1ZBQAAAgAIAAAAAADAkAxLsRzN0SRRUi3VUjXVUi1VVD1VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVXVNE3TNIHQkJUAABkAAOSkptR6DhJikDmJQWgIScQcxVw66ZyjXIyHkCNGSe0hU8wQBLWY0EmFFNTiWmodc1SLja1kSEEttsZSIeWoB0JDVggAoRkADscBHE0DHEsDAAAAAAAAAEnTAE0UAc0TAQAAAAAAAMDRNEATPUATRQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHE0DNFEENFEEAAAAAAAAAE0UAdFUAdE0AQAAAAAAAEATRcAzRUA0VQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHE0DNFEENFEEAAAAAAAAAE0UAVE1AU80AQAAAAAAAEATRUA0TUBUTQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAEOAAABFkKhISsCgDgBAIfjQJIgSfA0gGNZ8Dx4GkwT4FgWPA+aB9MEAAAAAAAAAAAAQPI0eB48D6YJkDQPngfPg2kCAAAAAAAAAAAAIHkePA+eB9MESJ4Hz4PnwTQBAAAAAAAAAAAA8EwTpgnRhGoCPNOEacI0YaoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAgAEHAIAAE8pAoSErAoA4AQCHo0gSAAA4kmRZAACgSJJlAQCAZVmeBwAAkmV5HgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAACAAQcAgAATykChISsBgCgAAIeiWBZwHMsCjmNZQJIsC2BZAE0DeBpAFAGAAACAAgcAgAAbNCUWByg0ZCUAEAUA4HAUy9I0UeQ4lqVposhxLEvTRJFlaZqmiSI0S9NEEZ7neaYJz/M804QoiqJpAlE0TQEAAAUOAAABNmhKLA5QaMhKACAkAMDhOJbleaIoiqZpmqrKcSzL80RRFE1TVV2X41iW54miKJqmqrouy9I0zxNFUTRNVXVdaJrniaIomqaqui40TRRN0zRVVVVdF5rmiaZpmqqqqq4LzxNF0zRNVXVd1wWiaJqmqaqu67pAFE3TNFXVdV0XiKJomqaquq7rAtM0TVVVXdeVZYBpqqqquq4sA1RVVV3XlWUZoKqq6rquK8sA13Vd2ZVlWQbguq4ry7IsAADgwAEAIMAIOsmosggbTbjwABQasiIAiAIAAIxhSjGlDGMSQgqhYUxCSCFkUlIqKaUKQiollVJBSKWkUjJKLaWWUgUhlZJKqSCkUlIpBQCAHTgAgB1YCIWGrAQA8gAACGOUYsw55yRCSjHmnHMSIaUYc845qRRjzjnnnJSSMeecc05KyZhzzjknpWTMOeeck1I655xzDkoppXTOOeeklFJC6JxzUkopnXPOOQEAQAUOAAABNopsTjASVGjISgAgFQDA4DiWpWmeJ4qmaUmSpnmeJ5qmaWqSpGmeJ4qmaZo8z/NEURRNU1V5nueJoiiapqpyXVEUTdM0TVUly6IoiqapqqoK0zRN01RVVYVpmqZpqqrrwrZVVVVd13Vh26qqqq7rusB1Xdd1ZRm4ruu6riwLAABPcAAAKrBhdYSTorHAQkNWAgAZAACEMQgphBBSBiGkEEJIKYWQAACAAQcAgAATykChISsBgHAAAIAQjDHGGGOMMTaMYYwxxhhjjDFxCmOMMcYYY4wxxhhjjDHGGGOMMcYYY4wxxhhjjDHGGGOMMcYYY4wxxhhjjDHGGGOMMcYYY4wxxhhjjDHGGGOMMcYYY4wxxhhjjDHGGGOMMcYYY4wxxhhjjDHGGGOMMcYYY4wxxhhjjDHGGGOMMcYYY4wxxthaa621VgAYzoUDQFmEjTOsJJ0VjgYXGrISAAgJAACMQYgx6CSUkkpKFUKMOSgllZZaiq1CiDEIpaTUWmwxFs85B6GklFqKKbbiOeekpNRajDHGWlwLIaWUWostthibbCGklFJrMcZaYzNKtZRaizHGGGssSrmUUmuxxRhrjUUom1trMcZaa601KeVzS7HVWmOstSajjJIxxlprrLXWIpRSMsYUU6y11pqEMMb3GGOsMedakxLC+B5TLbHVWmtSSikjZI2pxlpzTkoJZYyNLdWUc84FAEA9OABAJRhBJxlVFmGjCRcegEJDVgIAuQEACEJKMcaYc84555xzDlKkGHPMOecghBBCCCGkCDHGmHPOQQghhBBCSBljzDnnIIQQQgihhJJSyphzzkEIIYRSSiklpdQ55yCEEEIopZRSSkqpc85BCCGEUkoppZSUUgghhBBCCKWUUkopKaWUQgghhBJKKaWUUlJKKYUQQgillFJKKaWklFIKIYQQSimllFJKSSmlFEIJpZRSSimllJJSSimlEEoppZRSSiklpZRSSqWUUkoppZRSSkoppZRKKaWUUkoppZSUUkoplVJKKaWUUkopKaWUUkqplFJKKaWUUlJKKaWUUimllFJKKaWklFJKKaVSSimllFJKSSmllFJKpZRSSimllJJSSimllFIqpZRSSimlAACgAwcAgAAjKi3ETjOuPAJHFDJMQIWGrAQAyAAAEAextNZaq4xyyklJrUNGGuagpNhJByG1WEtlIEHKSUqdgggpBqmFjCqlmJOWQsuYUgxiKzF0jDFHOeVUQscYAAAAggAAAxEyEwgUQIGBDAA4QEiQAgAKCwwdw0VAQC4ho8CgcEw4J502AABBiMwQiYjFIDGhGigqpgOAxQWGfADI0NhIu7iALgNc0MVdB0IIQhCCWBxAAQk4OOGGJ97whBucoFNU6kAAAAAAAB4A4AEAINkAIiKimePo8PgACREZISkxOUERAAAAAAA7APgAAEhSgIiIaOY4Ojw+QEJERkhKTE5QAgAAAQQAAAAAQAABCAgIAAAAAAAEAAAACAhPZ2dTAADAJgAAAAAAAMB7AAACAAAAKstRUyJMSv8y/yz/Nf9i/1dWV1RWX01XUlZXVEJARFRj//j/3f/fzPl80W4Q2gs2zPl80W7E8IKDWzezLBNB4dKSY+QtxgioWovRVG+6rFaLWGtxaA9B0L55vS2FWK2EqEKCUKfBhrb/m9O7b8GK79xeAdwBC4Y9blet3OjugAXDHrerSW7Us1oty0gEyRPCCQysURPEomqgFhbWXpzWKFpsq/Lro13TogFFdHK1+IRXh6A7jMWIVv7igU8d+vbLExNgJYgN8vz2yxMTYCWIDfL8DlFTpzaqtWWUWRYV2m24QjBiOjg4pHKSiph2cCQ2JTMTsIOYA4sxCYjFWIOINapWxdbOVq22Ytobhtqa2GFjtTFsbLBYxdZiY4tYUaxjmiKmIYJYI6iiiKiKaovBOqwFEaMYBNGukaO7tJN4tUtCIDW8S7dIn8lNcmLNRSEasYgIolg15MqlHBFiAPAAYrav67JBDKpYUTWq8NWaMGaz274FfaRYbuPDZ4VCqIQNmGhHhNDqF47leBc1aIjYtlg0IEKfpTq+tc/wWiGiAmhURQBjWzRYgDE4FCJCLFuAU9e5qzuzwdPuyPwDK8Mrlxa9RwdSaR57uHanW9D4qkbM4sA8MFfxjUCPRCyAFfsV/Q1a84sI8NjmDQA04ApeCFTlAlhAumFSp0KgImcgARyY9LTv00mkGtbSxLQTJ0rMzExMLEbMTAJWVGxhWFFLEbHBik1pFqp166rWxEKNzGNpaC5WLbJAMUzrZhbaZKcyiKmZWRVV64Z1tTDEUFQ9usVQqhYFdAlWUUEMtoUKnLAKozJHivI10wJ6jANhaSGUvAVBY4wAKoyIb+enCsYColcVVezAawgjGREqohchDYaOy2BnYELLoCVALMEKk/aCQQaDQAjDKsCBFwsLBOxWy79AYDMm8GrpFqkHRLijqcumQAKClgxgcGCNvBrShxGRXFgIpABQ5o0Vju7StyBKN9Xuhlu6Da9ygJb268W8/xRQbhVQYCp75oL5HxvKUlfa+IloGjO/CJRLRkWyjEs6BeAda2EJBQqgBL4XDNEMSIAbTHvBEM2ABLjBdOsjeqmN5SLBmK6jYaPiOAjRcCuZORWLCTABAEw7R4vViq3FVu0diK8LYhOKiilWtbNa6sS+Zh+MKMSshiVplcjapqFq3SSSbsMQKUvAFRTFqoh0jO0mkBECxGAMaTQYUbGSqOhFVUCAIYbBMUDGHnExsACARQWxVlW7asFoA9FhlFWyJzSWgBRCCHXY9sSeFohgRKtYxTiU0FjoSAs0UKEoDEVoDMKAkLFhJURmpRsOINrtsrxEEqZxEQexRHxS8ZEjSHaIsY1pIa1WQiGEAWlAAkKLsU+iB6gAFFsaISL8wAiQjR0iuUMkwABKSqjRgO6tdFuuYwAZEhbwSvFANAIYFQsKriy+OdUNkwIcX4JuKOlj+hbBWKyxWLHwOv+OxJEA3tcL8QxIQNqQ+esFdAEkIG3IHPQgXCnB8HUyRjTMWqpSjOkp4ESMmUG6vmHEvsXM3pobnMneTJ6NWj+WNM5xFEXpVNuoFmol61imadWWI3usnktisxWmQa3qLJipYYjaZdLTiYxCxYIOV9sMWAvCIgSj0IXSqXhViOrEUkYmTJwY0AiZjiwgdEYFxqFNCFqkbBNUTYZisesKSIsBMZHBsUOskcYgBkCawnac2VkC6KA92F1ZjarhbJ1mVhazIFl4BEKhQ3I6RI5ju8CiRRYKXVV2qrb0BkloA0gAmtWZvlAEAEgIsVEMRrIg0kpVkFEGg4XTITYKJGyFGVgCTI0CA6Q0T35SpXQEZsGh7BAhLGJowDGAm0jiK+OIaKxQG0J4A5Uq3UdtypiwiSXZdoSFEQsoWIxexKiiWDAaEQEA2glixGLbohfBgBSBBXA0wGBjGjrPNufv4QGaBFBjRhoOQAG2FhwkZiCToLyhzmvBIXIGMgnaG+r8zjpRCU5G1rNbyrdzM8bulf1hmBbeC4y2bSYpZqaJBViMmYFyko+PQ+tz/Mv0RbJe2/8espq2mnTQsn87tnHQXInfGcXbb07ofIzE3vNil1F7VWSblow++5pWZdAlqmSxIkcTYgh1xsTcjOKwJ63IoYJW3NtIKABLYFYBEVHYxGOsDIwEgNyKaERExMaYNLZwhZgArYgRUUJsEC4BBKSWNHgUotWeQ8OKlQqDjXEEpDC28eI0ZQgcrhaAJIOQlEHB4DQI7LgtJLsXjIlDIeRQJoDE4AgswCtgAAQAGJkITBJ4tQMbBRgCgIAxGECqwehBBpsoaaLSg0j4Erert9cvlZSiJmERFxU2aX+X/rxiCKYBBCsDMJJAMVBbNL6pYvLWG7yNjQgKi2gNs18w0UJn23v4/Yscy6XjoPq2AwHgUACMFXcmG7BHZcagOKI/2GHZKaRMFeAPsGayBBl6BwnGSN8XLUF6FQ/UVPXKmrJEtJQaxKVXYmUMMIg+NG0plBAoh3Icxg5T5Jtjv8eiSFU00ng60RYaHVxGBQsqK41c1ydgMwoMcEYaWddL4J52IjKStoRWO5SNY0LZrLvQZKEQBwhCapouFA0qiEYjGL0VQS+i6G2MkKoOnSoiYnuWYKtaI441OjSKAIi1qgpLAGx2oRMlPLMqi/0euuxSkRdhsyrL/R46BEBsgphcixZJwMAsKjf6Vul5dUPKxB/WGItqDSgoOijRgoSi0yiKlf3SEFujFWnb+2Drvq71TNGKwRJOJ+x29QaEIiRQHbzb1RsQipBAdfDB2swsy7Ls9/vZAhZryYCqweBiiDhoWOyspmJYsWaDDTpQtapqURAEEo1Ghw6NgheuBFjValXffx0RRdEoisj+N9IC1Kb1UAOggNq0HmoAFADZt9JIdWvrANnLyXpSqaB1mshJQaaNGna2tlajWMUYw85isVHDKlZEjbUWhoXNVtRCdIpFRREbrWBhFVERsdkmsYppRawbCpePrOuk/A67NgFcYndloFJZGM0ldlcGKpWF0RwC0JZHTiaoAbQdoMUw1VDTUMqQ56EBTFUBVSNgWrcimKAxVkRUHRYQy20pd4Tlat3mpatlG7FYpw4BA6ReS5780jSWV8YoHpB6LbnOL01je2WMVKcP0BqRDOakgJIBpILM+wmhHFWwDlWjiipYtQERFrHG1hRUxRQrukVNNbtjdGoWLIKle82s0mWVXGCMYZxDAGxGC246V7C9QO6M+m4qV0J7AB9g2YmRgUYCF+yC0VCeIqpWoyiFMlpBi6gro6p2VaoIog3EM5+kIYZY2sSqFbmFzgRBIb4FsBKpBRBtqbrIngBMFourTLc9dYMjPFkszjLT7jEd4Kje2rpZBJkIUktQi8xoktPUiIKi1S1oEC16lN3/92EYrpakQbBV9W/399UeUEVRxA4VBUZAF3UtUldBQRZVXbSuYFQ6d84Utxt+YZla8JPOXTNB2A9/MrUwfYAsWEFODmoAvUSLgrUkBBEVnYqiokEREVUVG9VSTazlMje0isUilrI1IkqIaWNrlSDEIoiqaXtIp/11HVlUADQmd98EeCR1AcF4mLzmRoAnqy6AYsQPMFjB0EQG+oAbjdmFoAFH5lV0Yo0qFmvRglijooiqo9Ne1RjQwYcyGlWhGoyJCC2sRYyIqWohYj3/i7sYLlQWyy0uhKaZcy2oLJZbXAhNZE69AA4EgCXXMcagmQlMhTOn2nMIp3tnerpgBSKq6ULoQ8SI0aAEPhhpOn4bU8fdC0wqd1/MkBoBZ3hyuftiIjUCjtADIJiQGCPQRgJ9CLEaPAOPvGI7CEynP7AWFGusCKxchKuw2oSsKVZ53OlnegBMNnffdXqCEhCrl81dD4seXALi9AEQtMkwAsuPBIXlmNEOF5ZZVWBZ5vQz1iBWIgWkRQA4MwDL3RHBrCAdCEf0+h2vAGQ2yx6yEAE6hljMbJZ7sYQGkTHhBZhYkZCgKUGYdBmM5xifM8Qw1cwSWyw6q6Jga0R0RaEsvIqiZAxLV8YToAClApDCJlAVakHEqCBor+wLObZgrxQyr3aMajTzUnw8JjKv7okWtiEiHenNbwFNbLbBu5Rgz/mH9cwNkB2AmKmY1tQUdcOQzBkrIoo2xPfjzJHJriqe0gOi01MmVFe//31KdlUchyhCkwBx866GKDS2UWDjjCwMADrJHDbOiHycBp5BqSS+k8oNlhm9yDumN8ri+6lbp1pTRruicJSePtR/e/7w05XsF7kaI/MKepA35tpWk20pfKE8o+Bze7DSqx+rd6XCqbRNx768zznelfCev+77B/ed/vbz0+m9Td35DXHLTdVYN155mz/5Xjw4Y/8/XXj5YFll8fp4/3Hr528+aIi2cjwq2S97i7r9i+14qkJBBGXRv7r/deX+y0svHVqTk0E0YTbO/+oJRqtcRsZ5P8jQX6t+39wzqKkKqIql1JAOUgBEaL30NA2ZhUrSAoIhUWRSATadrioSJnRH0aSDsIxlhWOCxaQlwGCU899/IIF+7tuVUOdgAe80UMCHuzq1LAHmbXKy5Hn2gRLB0s2+3qF6t2lERSDerqD48siPrNz66299FlARA1zkeL4LSUOnuti73X4hb/+J6CE4PI657Pi0wYJvt66Wd/jDhTivb8/9z1dPJih3GmB6nkYVHHqC2u7JHThjVG9+/GyOMx6fY+hvw6CazQjcAoBoAZA3SXsOAOl5wL2rEQD4vBzg67tCXJiOLMD5Ad/GKwAAgJ8fwDL4IsaGr7583D/wny/v3wC+NADm42d12e+1FfnsFywDVzkkcG07AQAE1GqtoaFiM10L74ggDmzvaRvVA//RBYCjKBiAIqAFAB4FTCwA3qkcrA3IAvzMgJzdTuZg6pBnnX18B6T7HdWCIETaraZyUhiXowv8bNfZRqxL4et0TFkxU5jplcjGcIxU0iYjxi+5S6OIf59o8L7hheWpTFZ9A9s+w5nr2Zucfeptujtr3+Xb09I89XntaNWy5OW8Hno+xv75C+nx5vjs2Hd7m3C4+XN1v9d1XnBADPSLfHBdd0l+Vg61wRmzfDdFwMY4e7dSLCZ3p0+dijwRmydas9aqqoOXsoUZcCjcJlZ6gNX4Hvs/YWbbq8aIRIGgGTEz3Hb2iIb3lI+C3OnDsVvt53f3XerYGNSEgYWTGLxmfH6+0e73qrvR7n2RnT17BpgGuht2XSRQ3+fOXW6yu8gcsL0mQCXm1M9D24FKXZvz83p02ZH58HFIDj594TVtYJhkzv/OdixD5fjzdtOHI2h/fvQ24yxcAABdcC8/Xe8ZHl57v+7dE5SiFDAFGbNn7N39/v5FxnMph/ty2IKJ7/w6RBAH5yDwIlYQAHCGAV8eIFBhDDA/elMouajAtX+r/mCaCxkAAKBAijlBWNC5n//mw36/zxa9GVWMdN+bNrV/5N8meHdzbduAv6cYsI2y0dwBHSuKralbgHFf9U2vV++4YR8EMAAk4jMSKEz0IwD+yNyImcBmHcMT1Fv53EidYJQLw1+jvH7vU0JqLasdO+7jvq8uAxfeksJYfWdWi66Z8Ja7zIS8dfWuoOJ+NNayYaaQ726fb9xI9g6ewumgtaHdS9OeTvQSXnQJs27ZtmuPucbsjv93etflca3N9ir1bLVz9nxb8vdcatWTQPL1mazV/lfbJMKnXnoy9Pb0s+Okdlb1J0p30t5UPpX5vqdPk0PMfRTPvQ/RwAizFkvC0sVI6msmyu5jrWhYu0z9u9shI9AZNLC9ZZPz4XBu+VHGgTeKzfX2+ov4VgvMQ5aBwxVn+d9T56o4UbDYxNiAMdzMtxvILTruHRsYTcJUU3SuOXYSONUzpWLSaxbVlYXbXETnZPkwLxex8tbmF/nUSOVMw9Q+C/P8yNfhP+W/Ij/5GT1sDpZxvI/nDwAx45d0p3ZE/2HS+rLK9vS9Az+PFw3nz8O1H5/bQiQQkPORkfv0VxlW+r+fvazNZwuD3lyeb5bF5vpHTrKhlmT0REAASE1AGUw84KRENrr9XKvujZC/aRADAAAZC5T4wQAs8+u1IyM+6KhAfKAtW0Lnh93NrdODDqUs3ixUYZEKADAJIJmjdS5fyLy8zo4b38cDKPl1y4gxACRCAwAwIZWkAIgAT2dnUwAAwE4AAAAAAADAewAAAwAAAJBZlbkU/9b/1//V/7f/sf+W/6T/o/+e/5s+mdxQzyBtMZt/AhSxbTo3knYQYjFmaCTV5P6D1eGafba9fGdcLCk1l6KHKmxz+BjemqwhvHuXgG2lfUodA4Naatl81uK4fvXcMjo/aqANvz/WOGafmvnj7rWNjBfSmE6Ot3v71hftXj+5+HB4RZ98ftE8qXQJUzizb2769XHO8wTQ9Ly4dirHw+cF8Pe234d9u7zX+87Wx17KaPCOgPdD31zuStxVM6f3zOTO5ltH0G2/OVGwIiQC0639FHqC76cDzv4O6aYDc8TUv7Mlb8/7IbmL2DLnZDpSpnvKMTwUTV9D4XSOCUdRudTDcZwbexT0gKZ2UQO09/kz8uXkb+jsAwfOQPbwPllQ1JVdRwyNBE3PLKQ/EC/uva37y4/XvHj08sMU29Okchj4fpR1XUdLudrMGHbPhmUfIHgAwLu9htM0T/P09uN1/8S8mFad8UyDR2/drGbtdUWWHk1D4JQWcp9ru7L2z+2PH/4N/0u8S/lx+MietzE9VEIqANYkn2cIA6D55kAOUDAJK8AikYHv38sh1ePvX/q7hXADgIGFbUBsKUbyj4XhS9rIhQEMvH7cwl7D8hPxKWUXofEa+Lxfn4OFbDiO7geZecLb1bAiAAYAuKIAnpnMuhGyoXOYAmrudSLzaYKUDoU3VrT3EPIY975Ye2zvniebnXvsfl27VqTRkQmfixQ8j89ZltQQXkclCyK1n7x0bjYe7P1/flo9Oj66YRufzvS8to3z3rPt6ZFxFGqy9za1X4b+980Z1ivLur/909O+ePuN3TvLze3Jy7MeMvdzPHRfUpPVzXGh99TPx04AQ5Tqe+KXH+OXmhHxL/LTbYtqepfmAGsyERqb1p5Yts9nzhzqRLkvU/VEwzeHkw3OHzKMB7Te6itOfzQnPu5o9pDEYltSDgCQA+fDTHZXMZuHOT6QznlqpodDdfNePQBYfm9fwnbM7su4aTdQ3Pt9XYfDlZh7VA1QyfbM/p9bAewh+2RSPfUyA+BrfKqvtJ+URf257vXw/VN7aO8CzQzkZcXn+XXc5/TnbJS7My4bDDDAJ5sA4EyxuG5SjUMPr+UOyzyfE4sBYAf2kQOn3OPlXwZz3On+fP1pic/Lnf1VT3JIyjQvAAgAKKcCUQBDARJQIh9BSPL+ZpDHjKpVATjDCiwAAMAAQSRFhK3AZXP9oFv16e0jLgtVRLGQrZIoXyo3g2Ce3/h3MubGWDIAWLDdWFD/RzyXRnKOZGQKZYJAvRABAgBQ2AC+6cy7FchuJv9nsqM2M6G5OkPKX/CdxXhirc1HlDOZ/+nW5X6/X4rbWV99sY/Nt9P7ds+EtzrG0gYdrjEJxGIPVTfL3t/G1Vfd66B98nXx9hi3Y7+T8uLGzXcenfO3u784QPV+PrxjdXBjXvzq03/djZ3F1NKDuH142z+HGM4H6IjvYUbxD9aMmSS0esNJD11Pl3L2OARzHys0OzN1HzVDp8ax92f33Optsv2rqhO6VR15r9E1a5xmncmaOrlmcj+j54FYA3RTy+dRf2Jqx75d4ufv7e0z7y4v2yEf28+dXiS2jh0mwVYWPRefI8Cfhj8i59I8NDUHU8Cw7dPTM3n4HfvL4bJ+PtdRXLVC9eSAAJKCrM/+9PQhzWCGaSjIfKYfzmeOF48fHqbl5RbLLsPViQYILuiH7W7vfx7/fk8zW2ZDIc8AAa/+ASyYiUlvnPl5vZyvnN+2cetLh//58c/VvV98E5/xq/cxQAC6qMTvhNfsPKenP3X59//qH3JH3Sbund1kAjbAmXEiSlRIZAOhJFKxAAy5LfoWiy/y1qdZZ4w5ESx2BAYAxgiQ8WNrC4GuMoa3saDBuk4wKkmmlhczW/ABUC83E4ExBnNf9j1P4ORQ5wC+2RyIJxh5CHylBbUztJlvW0jmxvbfgP92qXn8iW8rGm7i7IK6AHOMrm2E7Zlo9S5LyqXdSmYgx5Z5o/GmH/vbaHS3vH/brX853tz7+mpuXNU9Toz29rte2Ag3zmn81UmDH4+r92QbS/6Hg9LYPEHCkOw5SG5V6foYwg4T0Mff9fe2++VjCVpZPv13on6fq64/nx1xf6sOdPH80UvvufV1T7W7IHPjnjV8IKeYzl8R/WTmSTEZLIqq/7n1pBf/W724lNpJxwZgXLDs0z/jzOmo53NyiuHZPL7WBOoplFPMXHWAZPjXTO3JYT9MEW+qswCAwdzC/2M9/ctXNoZuUjeDtfh0juVmv//3CyAXaoazr4vtS9fnH599kLDy7nSExRKlAdfmLa1pOxdL8XJTsx8Z6XoAHAEeB9DMnoGy/Zyu4+nHH+bvHd7s+vskvu1e/cRjindaBQDgqObMOnF6rown5lNm48DlPJN5yqKHh50JMcIQoJRmFgWKIbbg6UCXb4FIY3mxMCAxzffNRcAgIwPgB8uYIE5AKaPqObUBQDQGFEkAgLGwIoNW6sqJrFm2X58gAwlYJgDe6czFE4x8GNOPCV3ncqheIefmbP+A/7HRoec68f1+tqJ5bwx639x4dP0f/fBkbDNZh0jr1xDt3pXMkt0USpJAtP62fbfdXs7/LW4fEpIDc+fw3Tj6KdFEnYk2TpsPx+ZGQmbV//RdC8MZ0+d9rZdX52uuJE0xDf8d9gHJWGbDMCKuO3/1eb1Npo06h5ZfHO/SuYn829KRFMcxQkkJ3XXnVzT97drBkelgO8wuoH8Y7u9PVh0i0t4Iai7GL0wfX3iY/IubqMvmtPBorQImu74sxc6Ml43vy8tc/wWXjZlS31Gqc4Pz8g0/BZeHZO/ey8sx3r7Z+mdbjHDAc8D4vD8/UafJhuIGckOsKSC5E2rn7y9H0NBNgDg7Oj6u75w3h3ds//RWHkNun/fcnj0A1Mz/lH7Vwxz6mHLP01sDwN/dO/AAABmM96+3OKq109sPm+W3L3psU90Ww2vYdEBT0NSpfOkdnff0nvZSv8h2mW2+EnqFN6cAaIGmxAEAEvRh8AIgk+MSTxNauKNDDbV6PxMemViWMKbNDwSgjYAIep621OP1mFQOESpuExWAVgflkQDemcyUA1K207gLUvuHLrPpBpgd/APa29cMj6xd81ADF/7yqeqoX9a3XYaNCpJgmq7rhEnAduVi3e5r/PS9mLoYunXz9n/qSd9ca9z8z8nFpCs6NWnRzWv99ic/V7XFo5w/Zrh9EX6+pkON0qgbruHyz1RfANC5lRm/Ov+91VYk5sLx+WXY8/7CoaJ7gir2zJzos0XV5ydcc4rszSwNJDvUngNNF5mhrnmRyc1sHTJ7WX8dP/35rVgwLzkcbO37sd1fF14/n9Emazp+L75PlF1OtDtKqZeZZNMJAETvxUy/72cle16en1pc9FQPQFfC3Kfx23tn0f8+V/ZmqE4zY9FAU7qSQ8L2TzFsiWRXwrwZxvtujn2RR2ePW8e39ouqaXMfy6yT/yq/AxPrrmYMeADgMOABLEwWndc58V49ZHR5eo0ayPpbMAAkpAYzXCKVg5phXB1Vo1Jr1sKKL8J8/hjgDdDrrZfYIAtAAqCFbYyMJa2yQSlAWCwQRyAAbAM4BnAMCj3TXnFkQACSHHVMgbeQX2qbaAd+mczFC5jsBm/gy2wOtDvk6QV/GOa/c92l7ocdFCc2c97sXevXYRrJMhMelcwEg6abwgzMfDmvryzm5kFjcuj+sf7+PbGrVgaffkeb+VrDXzVrMD1aq7ePtf/vzrF8bGue/cPOt7tP2H8wavFBJ5LZ8NAz0zUx3g0F7D5ef9arVl7PV2+2g91FzTi+OMzc+M1kP4GZGlpqiHr/7YqjQ574F9n9VveA6uTJq3724fnCcZ+Ni/dKwS2nOX/r2w9KXJcoIGCG6eHzluh/ec6Xf2nyUv7b8Wahd+12UdmwhsGNF7Tq5NvFQE/L3f8BQQZjivX2xqee2V0DAMznmlg5L96Zh9nTmcm7FRCOarDBH4+nTg9tVh3Hzc/bY8r5xzGuqS/K3efvaxYxd1EAO55NdgDzAeAIaPyNx/USk36q3zL+bsXd3+H6eZ+3p7pmEtoUNFmQtYdoILPNooKbl+m5ubcsBOkV7AEBUFBCAJAVRgRjHCEALASiQwoBwgAQt+69qB+yKgAAURYGLBkWAhACcAiEDlfKqoBBDGDP4j5YA549lrigKJ55zMUrYBv4M6zQZjqHtQ/IzY7jP9De8en511UX2Wve4cJbaLKwjdG1ptZ1tVEpmcUTV4yZBOJCGe7n7+vgd3rtd3Azbx9W518nHZ63P/lY9n1va5vsUt8U49qXrbW6P39afEz/z5drnVWPf+9+XjxLewJgOM2zxOe+6/ex7QlswxRv+8S3osY/CLmPW8Tr/W09JuOt+T/AdvCFOE6L+9VHONOQvX797oe+gUGNYvf/VHqnAzhLfP/jzXLk05v/6iVVsayJ6QHEbPKbPZPVOXN/f/bpPx/oH3oT1Zk2xcxhPOw27lzoPPeMu9nhf96CU9C1mC32/70mfolZk8yChCJVPHRtSt4F+UBCAj2auhctcyJGxcjvMy0eLf16QZROPMzU/IdKN4eGD7XtY7hP3gIPQAFQ+OYM3BOGUY1aLKc1P8983Jn5cmt/XG7pvmqymAZkgCmyp2p4gDEgi/EM64Iv9C6iEYR2igInSgFTc6fMKtilSlv4gggEAAAcNSR0uzMAsbEMRDi2DGDAYBCPMnKscrbTxkK/RG/t7z1i7dQuSQC+OcyWO2ACPIGX6czWJ+RpB0+siP472g//rWZT+UkUhhszlGogGSPZRtu5hUVIZqZpl1iMGRhoxl8/9GF5n19ZX2zur658b/z3OTB5/enRqzxNXrkHuzaqeusvNiaP5s+yUXfevPr5zjY/LzrnclNCNZ5qDdfKcN69s4glIMGhzZ+Ylv3uFnLZm8/3ONzFR9H7qT37eOpEMLNjYWa780crj8jawxRvNXfy5D7RwIYiP5OL6etTTMxS/vj52huf/eqf9b/9Zuo+g5p1CgvwDD18dK+CnBmGhX2bKzMVgXLYWTv30zAJABRdJ73JyVf3/vmfNYEpIAtghIwbeLtyU55GMQVMzFiqIgeyYL2SnZlbuiMFJJJ942OULOc8l61p1me3nDEda1FVQTJ7AMCXWv+zuj5HmIXaz/AAsLt+gQU0JBWPSTKM4J+/Zpn8eWNaFa3HPvFehrTJBHM1m7wpNecZAwZK5BT6SnMggIKrYQ3JPvj/dSRYLYxAAAnAALZXYUdYGKw4ABZ4t6XXg+wUHj+F44gOeZw0h8KISoguAd55zLY3kDr4aeD7NNrlDjJ18IMVvpuk0PnPjuerVfZu9etIrH5koghmJnCJHYkXYwZSp12df/3ol739Ud+duR6vten44/vyx3i179b79sU3yYu29vuPZSwvvXr71MO/tjw+euf/f/jOfD7vxkgvBjA8p4uXvdd6CsjMsNjF3UcWrV3MmnLonZtn1/n21sxcKHO2iBrmoocorKjnJlONB77ZH37fAFQzNx/6Of6S9+45xeV4ef69Qb99JFf35VbUl/NAweFqZe2envh1lP3STBw4Dxf474ReqnOeaujOhsFNzNv6WnbjGK699aiZv8tA6ATObsT09A2UyO+UxDQFD13JJGAEhc+iIzP94/aWhLAsxwuxf/+Rf3zmiZDjzx9zveN+AdpLa+xnPtz7MWbvot8mYSGDROPcJXIAnETG5HUl2+UV5mHfeWX/zFGfD7wiBpEYWPGXLk/RMo//b09OeZ+XS16OCoWkaWgNSAawWUCLw8sdJTT0xGTApxX0IABgBAFnKSdwYIhRYBmVkFjK5lzphe3Yg1PxvjAAT2dnUwAAwHoAAAAAAADAewAABAAAAG5W/csW/5H/mf+G/4v/iP+T/4X/lv9+/3//iR6KDMobjGk0DsjfKNFuD8AEeALfbQ55aTjh97C30eHrVRZxRL6u6/lWAJlZdA0Nc5lkJyyJ2WUgtpKa+/s7+5ffPnU2w+xK8nFvrs275uLsYO80Tz62U6dvltHb627vvCxen3pDE7vvycWrGR4uc+hIl453X39+0l34ICcjzSMkY5Dow3UmJSK6QJz8/OnjfPhn72quG/Px+XpkJMVTHPjZhVKIiOo66cSHnmma7l1dw2+zuDblUd1R5fomv8dFT5RdFfMeyQE+j/AaE9wzxa++b/FxuHQbU9+z/FQ4ZeHBux/Kp/cK9Xa/TE9tnXvYbpPGwDCciMUzXdmAEDuD9/a/334E5Nt3F73tZwDhqq0s9H3fvn3dbiaJPIztUIMYU/Q++6xws87sYFBB9bMI6bxlx5SajnazKn9f8TCputxU0Z048CrF+mr2trWmAQMka7mCBOjGYPDkPYG/EbHZGOoNj7/YgwEAAi8G4DUSbkDw7/8/DqpMmSD9CqjD5YFAgDSt9j9MJEAWAPl9JPThNxK+aQzrA2zmbN6DDX9mMtteQCpwN8jtO9d437TvY2AU2d+/1YObAq7zCzjCr/XrMI12VEMysxhxBZwwCYzB36+yXX0dOa8xdHr8+cDQZur4Z3V8622umC/Xv28fR43bpFbfw342e/d/Wxr3+sZHy9FIZZQ0UxfV++s/c9yb7G46nv/Jvh3sI8MlvOd4pzV6/rz3ZOCsXI6T7sN9MX0e3vnxiD365htXnCfr5VBdXZRWluGZzCHFPUp9OTeX0/a7KfIaZkd9qgQYgPET2/HSedguL/fUo1Rj5mU3dU5/Ubz33cI5zCJnh5Xb+fOL5nNv/PegsTv34+N7SlOBF3alkhYGmf5JI5VyYgQDL2qwbRjyiTZbFdcoehZtqM7KNBh1BjFE11KhJ4sSFhFcK0JtgAXAOBdBaKQIU0hZMCTlFjH7HBN4uqVUJQ0tEARy9gZ1UrErzz4BOJqf1VGdPVlwU4EVOJnvOWKOTmVMchjyS+JKcNpYIELHKACvRgACMJX2fQFbqyiiiKpiCUCxhYQkPOSNsYh0v3o3OhzeicyUT8AW+MG49Lss5v0byAP8TUi/+5qH0MQDozvLFdw8Y6zTZ6tptLUIycwui9FiYkyAuqPH//f2crns3/aePU9oVq+zE/OO5339q5f97n2X/pujO/tJX38l+ad9//cf/+Pp2c7ueUljvqtm0EnscZmR7eO7+2sdshqSb/TDz/WJSOaI7tiUbMa7+hd0aXp2GS4i0RZKnPq5yHbaPnLzqNBbgHcCJ5l4LcTezz9lxLNn+qt4qZ8vX9vzzy12xjQWzBo7m3N/WPpcj2acSFy/CSScZBpymzvu8Hbjdepq33VneZ+dlWsknEjxY+REee+GymxMIMqYnv7oEekGSlTkWFbiZe2rdz9rBcOJl2tcHtqVOOliKiCPsvsa0cq6BpFgaIWGgBXSSfqSAKCyMQxUkdVOCbxCn2LugHIB5MEUQEafQj/EiyjQzlsaPYk+W0AAQQqrqn8YbBCyEZGd8LZ8bJT+n7QcvxD5aJtT4SILhhUjge1+ORGjcfHcF4C45a2/gxAAFD56DOYfwBb4gvp1FjP1E0gBfiZ4ty9XVXkWHvB6fxx4IOisH6PajjZMMskOrgOxGDPQN5nQniynJurDYuy+/k68D5Mm3CauxvSYg/u7eaO3tFjyM+9mHyM344yKfW710VMPJYf/9aU3r/K++vYbCpmBOe+a1VMIQLlzDWDjZ3Xf9lne9MOJmw95+fBljrHoJ+giaUpsK6/11OanzlzVLIXyNQBm7ZyHitx/BltMOahz1B/+5Yk9GXUnY+qGBqbzVted8xFveI8tvsh+IzYYcgpZTAFATgHlz0HfKE9fn8rrnWvP6pQ11G1tu/E2PgPwgTtrpjBXo0lwDy4TPsgNsxoyn5XKYjFNvStIv6OL5XN3ZDwu0gFrg4be8gOllVEjBjRtpIVWlYqWAFMArBgBXlIIHLLSIW04GZWYkbZXf0NU7XrmuacILxg3hrZ9ocYQ6tSvV9yfBhHOKaXXr81y/EebJLOayCQNCwAAzOquR3FkR3xZDzbQV9LhIOODlqFgaoHYoF+9JMjzdDDeecynL1Bb2H5uqP/mMJ++wEhlTs9JWX4fOeLhy806a3CfO2bx5zBmVpvc6+vHENmIhlldZhZjMZomZsCkMUXMDFo/9HbyNe6gyebdQ5S4xe8+bv/wqmds/Ynd/Ld///szfrFcNWd49LNw16/JJcJXpK7ub//kz08q9gVoOqjc4+/vZQa4frQ/H6a+j2HmdY/LZwpMzajSk0zMO3FSvJNuUM3xdZgsyKBK8Ybtp0zD2H/Ncfvnd40/TlvB3hBlQp91clSCuz+PfG5l4N4+VtvPgUGkaPW9XgczNkbu5459yX7bMvbY9r5211w5g5mBItv71XanZe4UfQb1kAEmUTIiEkEBicg1KVcLIy/btdW5VF79jpLOhIrJtsCi1JnhkBYeTEbgZN0kKxERTLlgIEImzSZbus4YkAFQIIMtiwggalEAkKBa20xgChIJIOh5pMoIj8MqMXqk1hsgfwCBZXIu6GhyfuxNr+2wpKuNm0TxqQ0SiwJhGbKuyrEBgQCkkK0+APiEmGQBnlnM1S9AKPgB2k5iWD7AJmcM701Fbd+x11dndm9WhhjnzeEOO36zXInbs55z+rVWa9uGZGbxtBgxMwNOEFP5+k1bnXuG9/bya5iv/++nK+uqDBh5Km+tFsvWHR8G/3c10B/p+cuI5cGPGf+82FYnBTCH/d4O/PniuJiJSIjx6bIVl/rHduyYevXNfTzN8fLx+RHTehH5DLqHe6NIka5n/ha9ySVuz+dvScY7ALiLvmOFjg587RJzydjkC+Djob/c7NxchkUS1yXPmoaaiXJ2dafmhd1vdC5z31l75SI7i4+TV83m4uDqZtPFMeRp8luZ0AAJFAlALRrw8VJcB8wYBAbS1AqGBWF5RsZT67i0rEzIMguz/Mkl6pxqE7VpY6LOnoZOxmGCsGVAbpQSIWidEMqAR0iE2CCMWQQOwW4MQE9rsAgNQbvL9oBCVidQ0iqm5YiRFspIUM5u915Gr2geYF6oWPrDlsGzjSEKAYEvumGBFQOgKBAzlN82lZRKLO012sS7MP4kR73amyesz5XcosIHnpkM/QcgC5wN6V9JzPlPQCqNG/hvZ35SFhS2/Of0Sm7K25tXjbfAPtu9rdZGq5KZmS7TzIAcLJ7srk2///SN0vuD9cNJo5dcazh6dMt0LHG86R/ywp27Gi1k727j/VIX04m5S3/KyN24zwEC0d50XWZ/M6Hk4ICLO88/SeQz++eCd//6XRZb30kejjF8uwsJNDTZxfXjfGpURTOfmnoneXy6NSdzfZVQ0XxOfg6IVH6nPq7ec2SEXvFJcoqGjj618FlELgnR6aFQZ3MOAw+sDWO9sgCA7qXfmjZnjofaems6GoDCMPZQa04C1T1kGplKn7sGwfcGQYIRBJUWgkWgsrVWHCL2dG7dv1K0RWIL44qR2Kq0lUXQSGEFkGAlxg5DyyVUAloLBFAAmYCbpIFZkQWAFWLAjB0TL09JkypIKXNa8TIiLJEGdbzp6jaNtAPrJpjZdN43X/8uWjocxBsACyIBMgKZVUbIASBHAACAVR9pff/9futVABhQBEYVsmmwq8IzAJ5ZDOMHJBs0/jYb3uUvmx6AEeA5ker7zszbz90tY83WjvfhXrq2d3asQ9S2DQvJzI44lSNml8See7hNTiLD9ivst42ccsPVDR9PB+ONv//m5mH1b2a5+N6bH7p81Z+vP38th75/89vxZ1WrnyXunvuhhvp+EOXMFg2J5lPzEy/LkzjeNwz/5p1jbkfZRujDPb7/nGRUnHcnnvGuH5WHf0YwnVDRM5K7dGbUUcSx0j+nM5/k5hpfdXPz9X8Mtt6bbk8nhkHcnnmgeeb+GI6veQnnRDdV27v/CT+zr9na3/Gwgk0/5/Yqk6EWuye/qThNQ6XpyJVTWQLXdFF6yR5iFVAwBeUqkTUhFRRuWIEcY9SL7FmakqfeyUVpEkoONAmtIsQ0YgmxQgAhkeBlomEmzcrIXo1aNpDpDxA7BpzbwQDACgYwTJZQBAhE33UUKgLnBgTsta4XB9iwjKTfXikHzRhbsQGwErBgWxTdHM2qWBBAg1CLqnJpCTAQwA0UYo1gpRSAz41XjjnlUx96BxjbEAFgGgckAJ55zJYvMDiNe0NuM48Z/wkCp/E+oL3Nj+JV+9uU9q6dm69ZREZVXZJZjFMEWIyB2q3Nc3c3DEWm9tpr7fJg+7fs733cMNTojB6+R/LwxuKXa+aH1zvJW7dq9sezPfH/Di21X4tx/UVa3Bck3R5R6vkprlOZMOW83v7/2r4tyQx+3MzLLY+Z8RIzThf9Q1QDKhG7uZzv9qmfN+tE98wp4O8yMbDBx/Oklx/ehDmG3MP39vKavLD4Ybazb48AEY1BvDDF1xD0w565nam8fvl2zvI6E/sl/DODN4EEEDp9b/HwTp+IrCmZfRdzDM6ZaxZonynNjP1hR89Y6AUMafACiulwRIOhbm5d7Do9XZGGpnBPjKqkbqPE34qLLHslx8RhU4G74ZDa55guUe1a2kKMjEJgEgsaGcUNnRqJjA3EwhIrhNVACBLp0caJvwy300UASC9enYuadKvSSF2vBe0YAkBAjIBo5ekAVp8KYAFRV4JgaUXP+xtMDbOSFQBBAJ55zMQvsASDn8lu/jeJQf+CISn43oxr3zuV2fNzYphfGmc402D365ijNd9Ga8wsJkacyoGZgaZ7vNvYfxpPen7F5rgcN+fe606vLe79mlav+J+bD9Z3vvzdfH/w81fY/uE/mP1Zz7P5OzdSLxzVJOjeuPtHyd5AE6QcjvfXW5fnPQdwr5J4378wBzio7rgEJuqUzn9T3MtnYoNuT8/h+3IgIkiAKHjkwTt0RsiW93w999v3VuQQty/CKHPQzsFLvQTNuOLpd7eq4n5mmJP1IKqZf1xMJZBDZwYlONz19oenc1z3TP2FKUTVKYAxVUtuTSU9RnKQTQBCQAbLCqgSMWhYXOm+LAwFIEAl34MTRGEZZVpUhBSMqlNmwFSXo13KCgNE4hQNIFuxyUGWwMbEMsC0GihEg+JOrUxuom4xspAQFIrgE2IIfNH2SXggFcMdCjzBOGYjdnzhWDiIpAlPZvqJYKwBK7ZYfILQAOxRLvoDI9syWZg2J4JjV8LgAwM+KczHDzBSGfw8If0yjZn+CUkG+HeD9q1cP+l9iNw7m+c8Vl+KX5N7Fk6HAvDb8GPW2g5VlczsshMBZgbEXcbSKye27Pbdvxn7j5O3jZvfB2X5M9r8upt8d2v/4aSN212YcVPm6ehhxmg6m21QMbNxTg3zAs2bmfsAPTGfueVbLNvzR59Y1izbu9O3wbTr3NjK/d+9R85OmoDgELF8/3fhfUe55iaz560eyGK4f2qedymRY+QnSkz6cLrra3505v2x5J4YgJyujDeimUMhly/1cLnHxxe2HrPRl7lf8yvvBFC6SbyAa264xQFq6s9/yk0fZUqdAGRV0MAELIOolhm5KRiIjY1wLVC4PSWHgIFZurDKokLSk3FBYxkROscChUMSpV0NYw9rZVkCVlTgnlCCgYkPhN1QsRzkPE2juEwhvc9RaRGCiqy4kEtwXk7wrdfQwpkwXkreW8k1kQHAFiKegHKJ5x9YMAJjCKX4fgIALH8WI2hRVItFeEJ2+PXe2yffJgcK4VEBAE9nZ1MAAMCmAAAAAAAAwHsAAAUAAABuyLnhFv+K/3r/iv97/4P/dP90/3n/cf+P/5ReScy2T7AyGPw7kH6XxVz6AavBnsHD6eG/feJ71Ucd6vlg+UHttsWu4agI5hjtqFXkreolkyzGYjQzCcTXsu3tzyeNY/O+8epy2e7/2Ax+DD9sOrP28ziwPP+p2o/6S8vHzH32276L3UP0lXdD87eXXXVwOu7zPaAdIsz2Uek96J0DzN4R3r/Th5cqsvXUf37W07xL1yTnNUFcAy1MREBe7vqbuufNPDOT/a3ejrqKUz392VmfuZf4O9lDsEWxsxc+k7msZHblJHWl4tRK4v1wbD9RBfeeF+TWzkvNztbuJp2A7haT51xyXf6J1r2NNWU3Lbhni0kAaiBqSU2MMZBTgAREJCI1y/QMVAhdi6DpxElTxnMPUSW0ESAsDktWRKZve5Sh5ArGrGAkUKzBKQOiAABIh0PBYEsTitrXTZ/PDUSssSMpwFjYjGHQEDg9xywowcS5vcHjUTiI+mJu954lZlm34RAlF/u9e9lI2YG8lfVZRT5nHhkCaXVoydgtWojSdBRVOTa1KAAeWRxwX4A7+LvBv0m0ywdgHLy/kP9bnr54dJxm2j8FF4Fj5dfRdVqtHTJCMrMYE9M0MUnCSBxGZutfY+6hK/rX+NGevv7f/P8Tn9ia7u+rbO7B7LXF5OLuxE66H7+5/S5+8ebglfGNEUjXnDCAp5aa5IpyANhI8vNr+5c0vpalOlXnU0E6/2GUWZ7n2s099cRkJDu5fM221tWdZlPldO6baldW55uj2fmve7pwrarpNLuY5sPFB4JpkiQNrJX8f8/t3vOPuXu4aeSFR89cwq3sY6l0f2LsxBT/VHUc0VEvTjI5CWIKkRCwpKZxAAUJ+zVDwrKqLQgRZBw3OCsyC8YwhDApJCjSBmUsQwtoQbZxY4WyANNaUSWAQCFAYlYsxpjmWJrJ/dgFA4BjGrBZla0t1vYYgRlMDryiqhxRSNiCdcZoJW4QZxOOYQx6SleFfNie731Evf/Du5Xap8JhAEZ9yA6RtRpsaftLkgFWwNjsCdnQMZZ6fl8CAN4ptNobcnU2zwP+TWOm/YAhcPx9If9vsX53Puzs+pp1X09X34brfXolc3aOkTXqfYUwZmYnNLGTMgPMQVXKue6drnPr7eC1T1ea25nTbrzxMzTuztauPuInNS6v3L4ZvbC/55af33l+b3fG4RKNnUcTiffMDxgp+/dnZ7wABPt+PdzcyJMb4N/b2xTblZSDXHP+NnPXcVHPZNyjQxq+5zzD7EI9mvo6km6G7Y3tJgkyZzKW7pPveVPY4G9s8+Ltwgxr4703pyd3+e4sFna6P7fie8t+p6NT755K1/3PZgNEmtJJ7Jmtui/nNosIkoQaocwHu1DaIJFOogY3M65WVV8hULHJBicGsYQFKZdQgAGJIdfERDAI0k6oJAIYIq+AKAY9NkiAHeAQysYAIBb3gCXaJgwsVCuojDRy2dAMqY8N49BIwlb1eCd0NgheB7vFJMHjQ0eAqS/QAqwYGaMDtwYFSwAJDFrAj40bvhcYAQHACBCOaHpyfyEIsQIGQAsASMhCnEF5DoUjAD4ZzKQXJHfwfrLCmwRmywtQB983uLevd/eJMjr/BZWi3Y+KRlsLurWQzOwSM+0SMwGO5v/WN+vv1xMeEqLp7cvRW61YvuLHP5vhb/4DPTt0w/o85Ux9eHjy8O//PU9Xn//18+nXe+M4324y1bwlgcqhvu/fcyV3AkNu23P/s3w9P2cH+bGNQ/nYW8nATUZ56iRTaPcyOaY0M5Mi3XRNRm5mjXN+n+LcHKOzL94jj66nusRl+/xCqBNWMVEL9vI4royX9a4u/mv8WH9N4Oq1c0qubL+1Kq/ucjEYNEVSmWyeRhMSJzTGeKZYKEEWo4UJ89srqpFKU4EQaFQG1KEGQjAIJ46EphSjskJB2hGAILAbAQMjWCgBIm01siAYYWRhPFrBqmIIejiAUywYjZBNNnQbCGgtFjEn8rVGtabInuqize6J8OyvFSAjkFlZhDCWVkMk9Umg4cITBOH3kOQ1m4QXIgAQRpJiCbDAFjy/yjxb3sEmgyI8DT4AAj5pzI4/MGiBn5sV2slitnwDBHjekLb3X59BIfZz3q4NF9WT5/jN58O3rTU0XGZ2iZ3QNDOgjdf9TPrLb9rH7VEtfupv8N64r/1+lf7pgYb57eL8j3TicvRevxyN3cH22n2sXTX/28/j/E4uvvEZUk0+pHt6hzOSCRzmeOhH1/XWh645YqLmvk96ZHvbITAvMkOPluVKZs5JvWs2wsxvN8xR2p0EJI5k+tsYIpS5yuvZ7e4723G6jiUP551jJDDT/TRwDux1+ALNszPzs6ca9BAjUWdCtVB1rKLEAbpdazE9/c6OmBiiFoMoIJUtFFd5mjSgeGmUOIGQiYtRaCjZ4B6FYCDu6ifJBUUIE4ZyhCOJy6ZsL8ZQWua2JwRkI4yActoKAYHDtm1EKOUNPLITnCYpJgyJQNjCTS+GSECnB3JggYAB2LOpXKSeBw7HqUPznPhNUx4wWILAUaB57SEcrIcUCQAMIGsxfwAEEIBZbWEJcLn//kEAAmMcyZRWedBXICQAfikM0yegzuD9A/7NoT1/wqCB6f2F9H946JMiD1mjN98zIsYYnT0Z9W3N1GoWQjCzeGJ2wszAlNt/U3PoWn80cGesGtM9ieG1/1I1367RrxXh3sbsb7/82L7+1U+PT4d+/u937uNtDbnm282ztOve5opf77qWrJn62AFOzJP3h23ERdli2O+Q017yns6Tzs4lHncHAor2O0jAFJeZVvbPnTsN2Z3A5otTFw3TI0evDjLh2uOqWI46b1d2wVJ7j2cRRdZ9R65+7nr7pVHn5KvmUs8Dbigm407Xkoopcl1yeujiUi7vepWRGBgoqLYwxiFZGZQgBkEQqqzdMm2CvYWmqrQqnCdIRqsk1nY7GRGCADAICBBYbYcbe2JVJYMAkQagMR5iAApCAGAAKUoXto1KyEnitIxCVIp9VshUgFt3fiGoFgDK7XWb4/aLE5ZBK2BbAJB4AcHm/psVdrThACEQIBxTYefYmPkYz9387diPFQAYCQBeKQzSGxAO7iek/+Mw7z8AD/DzBv/ttLdc6rhPv3VX3FicbAo16FpRW82qtahkAnbCLjEzA65NfJw14fc+v5sDX8bK3//KaOZxNPvexX2/f+CWlVB3b03Ori+b4zDtXouLj/6jjfNt47j4lV1LV6WSq6heOGJIgMRVw8uHM64frz7Ry6PLoYcKVudb/cp45DJVWea4f8/dI5IzPS/cVz64y/8e5bhfzlKSsCd9vGawTrB+HHOrekFAwcwQrVVXGl/1V1/8dc3FPKlC9PSVV/QMIokLjNZYHqJ09irTlbU9/AaYG51qv6LPUBnxVD0dQg0gdbuhFBeqUaiwlIyngQW5BWBY+YBkhABd6DZZLCvOWQpCoGwD0MgB5akirwBYa48KMwywgBkaPBAmaJpEiKBghbQ0AgAvFkbtDT4qRfS8bqMIRSEr8Bd75xMiMEEoC0RUuc1CCAIuOCGU0aoVI+vyDHZXq0TqmZZvedYSQw0voiAMAF4pzNUvsBGM6f2lpP6Rwox7Axrg/cMK//31eHr18VIfznzY2u5H2yx81UKF8FKMmWYxWowZ+Evdr/ru2u4jb4/iGvJZ38nHYSy9PbK7tnJ2TXrRK26bV647f2++s/PEIjib+XS3jo+P/nhZxO5OfjNN227UXd87Yl7/9AyREMwugujr/CGHnfz7IZMFH+epe7p2sy3ccaWyqzWvEhYumPB51PdU42kBJBMf+8xn9eTja+23Ly33nHC9uu7Qj4eFm0QDKdpDRSQdlCOnQ/m5Rfg3y841anK+Mk3ectC0bkbU21dnGAYnCYBiJLWLBW1jIxTd1WsDAKCyocAECmh6adKdHmiboHGT+zYdkiBbIpGlcJVWAwSEKnK5q6QciFfsiUpRWcSNiAKQKzuNBQIFA3JpAEwAgKG6FQkliuBtoqLB6PDPisA7Egb2V7t8b0RyjGwJCTCJwbtLfgutv6LOIkGbrI4dhoQOVyHAWC1Iq+x8MsaLSkg/MACeKbT6B4CD95dh/V8pzNZvoBS83+C//hvFM1wOgLKzHaMiC3FqYdJldsJixC6TJHpzfZ0tVzcmnNv1EDN39H99bF5tmd3bdG2N5Kx2Zm/++5xWOajHR5bPD22Sb3YGvvLfSG73uS4sTj5T7hUoBS/a0AmQdn/u53NJ1QLbtivlef7guMjnlmTn13MyPlkHfkPnbNUQZdV0xPAUnxxYOvOM5yl8CNIzOpC9TP8v6Xy5X/Y4b4C7YyAqQdypcUd92H5+Koe4dm6/ZFp1jarghopzobSsUB6hiJ41KTTTYqyLLdXxjGumiqTxJDhc0pYUMDIJIiQCqhBYFuVZImq0ie+ScyvVsiKNhOyiZBhCo7U8oUEYGxWXnlA9gwQEThlDIkIEhuoQrUNkKtIuJy2kwEAEWWCABejYzpIRNgALbawJtJgDsOgCgLFkgEVwdGbmAFZ+HtP/xWwHULXMbokfbMngYAEDOP92nD6ksS5YnioJAF45DMcfyLXQfL9Q/8pimH6BjMbk+2SF9naCMwSK70OPqKKLGwXm+H4HjDH8GFlHw0cEM7MTWox2SRJI142fodhUeu3jIRb3m4YPud9wII29TviG+fgEv3vPerY8qF+Zmj+f6N6aB4s32/nNnwtP44J2R+7o9P4ITsvz5bO4fBiAoY51vz/bO8cMYvmqn5+zR/QO7Wz74plBhrJ/Hbh3uvNvHN7Oo33f1Xlqz8Mk0tFy2O8+M8Nsnxu5fxzr3/RR8uWrvR0oDntUDMXuUfkIf7b05ppDuU3HaxxyYprMrTh2dSrJvrIgRgCxm4+C967SACEWVGtKWFUEJr0iYebqwUBKgGOrm3YBiqVaW+M0QHpYBZFlGdCKlHjAQeApq91QRz+ye5YxSakxtAcIAqiMUuAYJU0oIgorEafOCDDQjRtHR0lbCDpMcDmFo0c27lZX+6Ubp0tu3sYZkpKiB9woaR85bWgHGNkyCS2CaX+YwQtgEJaX5EVY/lojT9mLCMAAyBCiaMUCQNjJczi1SX0DHlnMpf+SN8jYfm7sr5KolL+Q8bF5P9nwP3xc7Np46KdxZpz127HWvtD7dR5HP3k722EVRWsRLlODDookAVQXfbav++ZROV0OudXVFnGNjLJdO8b07vvpPkpI9oyGWT3pX1Qvr5afHWx55jXOeL5H1LeLCTZpywG+9pizHDcAQC8vbw03l/kucQree/PH8dvfavELuJLmD+9Pmak51HqYc+jKe9/ku+tziJmekbPsMxtLQexk93a/XQ49v2jl7Pa+zncGSLwh0z+bM09dc33fn3j5jgPTf3PNXaAyX5kL89dkMs3OqY9ZVe6h6P66O1P7N1w1sxM4VY9J3kHX8+m32zqTRb2ZAOQIw5Vyuf+XK5IBAYbAZLmNOE64/Nuetn51unSzgbtgU9DUOHsgL3QIKtvJQNJMQvbdA+5mcZUCDriGnO6iiroghynvpHMe+zZ5U6N/mq5TJGCxhSGA1gumwMCkXQwCAAWcRwAhwqQx+ewDfsUqyYGRQUBdBxBZggdkC2zFbj4AihbQIHD8dCoyiUpQAE9nZ1MAAEDMAAAAAAAAwHsAAAYAAACRASPKG/+a/4z/e/9y/4j/gEpfXVlXV1VYVVJZ/5n/zx5JzKb/gUwf2vB+miryv1gcSP+ELD4UP1+q+e7TF36X93Pv9c2jue1zVDTamGphlpnLVtqlSQD4v+4J00+NEu7uTjau3V/t99ZqD1frl5fj5OhhJtl4Rp6dfqdf+f3/2hjL95rHPv+w/Gfbm/San99zisQjM5H+FX246/nk9TIJgExSfHU5z0UrCU29f77jZ5wvlPLCD/RunCdhTud6Jrt6aiJ4xlH1RVs6Ol9/I9lkj3Hwm2Pyefz8t29Z1HeaD9PAvu7KzHPQMpu70DP0fE2edkaZW3xcdd54hiWh8g/zruUzHT/q5NHpXOiq+R4ARnffBJyAr/dPk7zJnk1DJpPbktEIAepXvvXsNQbXt/uSb7qg+dutk9j9Pz6eZyDJt1uNh6cvdh5J+wq/AjdnuWZ0bcczTV0wG+rMqc45f3p5Yo2jxqz3MEVTp2bG40m2VylqCLbWs86c5Yo8DHYfALsAAAAA2wYeDE9wgQEAsIAHBCADxvCBI1T4T5VNjG0D0ianILAQRqm/jb8C6wBUNgw4AP+IBIHtAgFeSbTrv4B4wPsL+a8k2uVfkPJm8O8Lub2PDVfRr5BXVfMaYc2atZv9hbFH0zHXtrW2VpUuU+UUmgEgffrsd7zhNKrpq/OXL61Tl8vZk6/t/+9o9WPlqu1rzjGMu437O/a9uvorrvKDvrv6ze5DOvswx3AKyLi6/+UCJTQzYV3P+HV9/nzOO5mPdXLafUHmnKOafkwciSOC3Do+piLegZq583fH1uIA5QSbWgofZ6k/28669nZQ7/S3NPe54QEAmJ/KnHKSzXrBBtvcEnO2sxJ4NHmKxsuqSt4lFrkmdb1TXb2S1ZUVrdlkAbvH92Z64AYmZbb9SWTK7fZAgUE895xh7F7IEom63BUUEhQLpI3Yjyu7VJOZjaYzNthOIh0xU6NGA2XiQqArKdSpsHdn7oJWrZMPwCmXiCANQ9HQbFjBUJ1tDRY5t1nWZcfCJn/j/qRtQP7BWNiAMaval3/owaEAAQDqJCO7B8UwpABjGbxiHztP9v06wZBXCy4xk6O5DGAPQhiEIyBjGiJ9AR5JDOK/YP2m8P2CP5GYjf9Cxg7+3uB/9/6VPre2YV+8jPp8Z2SBy1FPAb8OP0ZtDG1Vg0kWhxIjZgbQy3lbp+4f/7rvvCv1PxPW2v973JNOXeWu3D48dNG1z9ztV/d7uz6wdudC+pGDvzvPeSThGvqHTO7Z0+TnDgCUzPfMOd66OaqryutTWn0t7z5VyU+eajQbc3iezVu87D3f2wN5VS51zUzNLoBB9JN1cvO4o/c7LquU+eaP7WM7XDnmgQHE2UDBZv92V/Mo2tOXQ3O957DFKrhONfLPPX2eBQ7ZM+l8mF2TYYsaN2jquanH1Wb76tZXYzHkAIRPNNBJghBNi5ggy8DVKXQATSYJY1wdOzn8MzAXbTtiw+b3DMxiucE9i0wohgKYkpNEveIoKYtJIZpcJqt7p2WjG3V7HhCtXaX/uI3XiI15krgXQAAD0ngUIOedyHPuAYjiHcAQIarLE1aawCCi0nrgtGPRlgeY+b/4WwPwWgtd4wAMAH5JJOofyNnZfH/YCbePxEz6DzKxM7efP0j/t3722xxrZNes2Oeena4G+6jvmtW01giTTMBOWIzFACAv417vrdfl65xr6mLiffhvbz0LyxVW73rTub8ePt61u33M71eHpC+eMf/dbPdo/clK9iakygFLFM97orzSn/szD8n0xc/PV5PLs2bR/5k7/l/X0rxTvfP9Ua9jyJFbvft433wHy/e4+Myevc7tGtSm6LcbztUR83NPZ+8aQok3uXdKJApRCA3eh7M+xdDycY6y/73WE/C8uyvzm163v3d6j0as0fRbGqI5PUxBD53HKqQ97m71c1V2Lkz7z0t/i43BTC2Ne4IIsiOgusGJV4ppA0W6d+Od9GtAZmCS1WWna53BK9MowIhmMJCKABCgMDtKaoh6QgBowtIYMwwiH4BFA2Niiy2IYJhqEqUQwyF3oB5FtBwkIBVcKp/DFUBICOzKmgqDeHXGQNACfQGK8r/a5wb4RFBkAP5ItOt/kHFTeP8Y4F8k5s3/APUwkp8favnfa4/+273vhr4fevPhm+v0DBeHNDDm8GtrbRsWLjOxI2ZmAgDWL7XGD0+VML4+O9yLuq+TbeNDYzfwuzLiSqi7nW0evpujvW1V/Pta6eT7GXaqPj1axoNHAvvW6uuxi/igYOyEdPpntPZv2b63iZF/+dzPP/rCJXq2E3w29IGcy3bs5Bnu3z7sDbXHTxW8irshu5O+0ZxZIj0z3a/H6UscFh+n1ctlGq5avRDIYugzg58+U9Sd5ORyx9N5zU8/34yJpzvWN3t7HT/VOj1diU9JT9J9KGzefPg7ZCQtzKw9C4WiFMKiEwFS9wPKTi/V7QaPYSyagGawwdOOS8IKyVhODQAFJEvPQgHWkm4mA4AYTygirFnBIKmZWIQCuJLYVqTe596wJUg5BhSVoFmaGGDFqcWIYWPQzxUJJGDA8lj4HaXZLGQdxXcpKZSwWss1xh5TzlRrC3YtCAKSpkNleqyGB0aqtiW33U1PtRTeHAD2Rxz0f2EQN5P3P9DmEYebzSHTHfx82dD+AAD+1tsv/k780aNcr5Lm0DwrCkd+a8dos7ARoZKZiQVcZiaBzsvUJyZzNdaeq9eTNQzurbSrUNbtEnaLact/zt8LTn+tbwMX3fvcmOWMtWOGNEe9t/b827feuvJn+90/sfvEs3j+TkN28vbkvvdkdksfoMXFnxPcw3fDpSvY5o9OQUR/TxkzUcfIzoaG3VdRs/Oxk4PJnwyTPT3XBqhmSq8OKpIzUIHW7rg75Me/YY994hCwwphO8lpjupxxZFvFLr6WaR4qCW4omDL3PfA+6emiT07BjY84VAPZgNMwEMAY94ohgZEkqTDDRALitt5SKU7ALCPECmEyKCnZyCuLARKaSmIqgajYU6cTEWxHkw4BLGQRgopICAROpnxYDVsAEIEBsEl4EDMldTGHtWcshZLgQiAhkrAgMY96BWLQAMBBtxM8wylimQoCHsYGQTQWjTFg5AgEoDhgd7GkPH7aHKREC5oAHBT6it4BWI7MY30f78pPkk+5FSdpfAAxjDECP+qSjk7+3EP76h3ZNvqw7fFwxHypl9cn72Vr0redkIudASsPQEACkSFzVRZrw3gNBPoKHtqQySOwpY653+4XZvtJtwD9C5AXMgEL/soBagyeN3HB9jWGMn65Pl7/LYfbv2o1t/et3sTqrqKeOl05V0QjCooo8VqhEVe0RtYYCK0MfgBFo2h7ml9tFY1SYwME+goWwzCptPRdNSv0eWepBCns43TZ7AWIZanVDP3wBdjH+acZp+wrzfMyO+dfxkPRojfQr2xrrdoqVRHTixaxKKLFogiCimS0AqFAsbQvRLA1ooutqwGotvu0xQD0/XasUruZpLFcrQcWf76ZTD+jdXI9+AVYoqggGfr1PCCzrlR6piXuZhKNb2+R05xfv2dLizZsFdFqNAa89nKuWmxEAU8Y2lFUGGpu95gTa1lTi0hjx4f0EjRCd0tbCbvLY7kiL0qvGi1Vu8vlmvwBplnWkkvU3xus9+QnBJIYAWIUtGuN1YoKVrWl6BXBKdasqKkaVf+qKNaqAKYa1ixQtVotIt5DuxFScS4YJAkrASQmr4/gVlylDEq8DDJfk8CnuEsblPi8ANswDC24nACh0vQ10p2nvKKGnSnWVSxqoUkV0YqqGGssoGg1CGJFq1GFAcSKqtEq6ufPX5/XrMqlU1OiaDWqAOQRrz/B9c5YgqmNwh37O/yZkcToPsC0MBcW1AD+nAQ2afzkgdqhk05MVEWrw3HuJawRRKvYgqIVY2u86ptD18PmbDVUSz2YwBhR2p2IWtho12g1AADk9cpuGpe9feWLflCoO9l2e27vCOILELMFSQE38w684I8dIxfn9z9BnDPOfT79EJ9oOf8T7kqFoYpWjGhVKUR0E6VlViTLS4yt0CZCBlYDVUosRlNR1LIhxPWueTbyeZILh9QyB9z5rIyXnVlUighegMGcJxKkgF9yAAOR36K7xD8lOl0XP2XV/KOfFbuiDFZmQVvWFBrWDZEyesMrgeQBnSBiC0cIGK1guMs6CBTuCmcJtytXKG8Oc4W6ohksz8qRP4e5W796HDl2RkIrDIMMnL7sa+NDmd/q+79XdBDVOF5ZFFQVDSqIcB9yjyKIQkVpW4Hhyrs+8c7poE9PTwvE8QoWu80klIs+SA+FZe+UFSxXSB8EL4AoOJkgAw+ZYJ+/PcJb2KZ6t/GlrPjHu7eW7ADLsFfBEpsjp6doFGxLjDWoomhrjagmq5WBFvFcENRSY+0aFMFqCvpHHF4eMNgDvnGQ6Ls/4kC5wkV+Y3gb/N0PACAqz3/y4uN49/2Id93YV8VBP52t92GlRghvdQkxJZkO6ZJA8jL7c8sVCXJb15i9cfNgnNjfbp9KHt6uW7rzR3eH0RJvmU85dtfnT53/+vlbcjM3qhaPXc64+87HqHfrOhwLpgq5uZw8h6uSKmBjLc8jb4iE9GRO7R9/562si3GTjF1fssbchfKMqpCDSYiMTGBBDQsS4RoIaTVSCzs1ZlK2cEhhYQsEaGwhBVtoBVmIotPuWrFqNALYASgARCqhjD4bQ93PZkFf8ed7Z9wxuu+f71KTLnmuSTszSgC8rtAAQC2S7V0prasoafb9WFZWapl7N4CZ3NeGKror9ORCZySq5Pl35y3U3/mwaVV3Th9+ephjnufYHhdOzZeHDw8P8/zsp29+58yAQNJCf7+numEyWgCgvLq6pcxkyEwc2t0UEEloBEEW5YFKBVmkUigvY8mwyTpl48ogdL9EPk6GBQIIGGMggGg1AAIsxKoQoiUJEa6oHpARIGSEicgXAL44zKYDrNnR/BUSJPebHIZ9C4kbJ4REfPe4j425vnj8dCwd6mMBzNG5+rbdlsIXNIdSbpKShFaBdVkXJBJjr48Hfy+O+rup9su27d1e9fc/2e+uqsnw/1RnDGxca858pf58uezldBfKtdLX2528flyYf3+t2O5L6b/tuf3TY5+PXH/3zv3iGfvA/TNn+3mVqO8CiQSGyV/1bqVfeFmy9y5P8/uwnVcBnDh5fzlKKdIjVTVirz85hUpKnXJSPywgYuPCElRVMEDOREMle+f+/LCO4G0qAAKU1Z4Ai8k19q5vjaW7xJCXcxLAgkgomp5JQMJR8jyfPeOC5xBhmL27EfS8DQB8uUbMZFLD6QUOALPOAtBMUQMFE27YhmJWwEkEmd9cuN8MTx5evHTq88O2IQCQl/2az4eB8rRvnTz/uHiXFgcewEwPA0BvY5mdvfzdPG8Ofa3DN3BsWtsBYIqNUzC/K7lkM0O/O/XcH/sxX+OSP+flmWbrAxAAALAAwAPFrkAvTVC+U5iZ5+m4QNxghDAAZxg+DwDcCP/LA/E2mw2o9hUBEB/eyY/f/1son1ELFFksVZlfWgC0CCzgjsGvUGwwoLq/qGxFgj0CgACuC09nZ1MAAED4AAAAAAAAwHsAAAcAAAAxS0WmFv+//7j/tf+i/43/jP+S/4//e/+B/4DeKMy0FUYWZvgMDh25Pynk0gQ2Wwz+DTb893vG9cTG9U2kp0vBZUp+812Hj7A1s7UpJBPu0k2pIBi94yYnz37/3u9uJ99qhz8X1/dOqzh/9/X6d5Av3to3z1/jT7M5eTS4Ofu0qPStl8ZvfWuzqcObXsRmritZm/VX5tpvPvFGbkDD/mf8+/fxw0nENlSGreOPevoiD8yyxyCCmJPdc33yRDtjRp/jrXaHnvG2UW5nO41IxQuZTckSOWdRW6yccaYmg6q7N96AYGje2PLMQS/3I+o5blsWaKLZ67dxi4lVyRxMYUjWkNOP6WfiJXLvOLp2isonByAfAc73z2L5zFN50kOzk8MFpPF+BZjrrr01g4GOKinX9+ixL/nhY12+bxH3G+qT0AA3Zyxz6N/7rx/1VkxCc+fXJoP9MTAADOzzD/pTy67P+6+P+tC0SD6+7/D77wfP45vrGYZyQ7JFkhhPLvOD4WP5rGfvlSN/FZ2ZcQQQvJFAAUOAlwphAJj+t+GxhXj5nb96/R4AhhhZgLH28zng5c8GG2SoywZgjuyLXKfNi9ivB8ApqEvIT2HChou6hQzkXGAKKK5oAKYIAB5JtMsWjNlpfEGX+5VEri+QpR387YBcWw93/tSy+/48x4vLtTVNdwGFqmN2hLewLSOsbXOZbReoa44B4KNxVaYTt202t7GkcSr/TTu+sKNje7f/rGznB09s/g43ewNHbWJvfsV/vEHzvYcuMfW4kDR9vvoS7Zwl30TQpKaun6tX2zt5YZ9Lc9zV+ZnnFFRl96/kGmmmIWQm41/G+RhJLJegbYTcUx0n49zWLG3aAhfvZU9cnuza99qdh2mVEdBJDey7k/bZn4uq71Zd1J7d2PC/n6khQdNXFWR2ZEd9N3XkKQPv18xsGMYcetMAlf09MGtm9gFoasX8ncXttrXx+o+C9vfpgaZLNf/sHiLnstDxR5+QK9QlpgGIeOzeP1BVX69sH58Jw34JC0CYJyZZ3uRb/+IF+vZaJvPBVuXWzUpF+ov5UP+7j2QATTAzNXXXcpJu+RReHP9+Xo6+P/f7bciP3MWQDXM3aCcAAqTEEJxkAcBGhwcZsNgYpWHylfxr7pKib+aPoirqAABQSG9gAVv+ySUd8bgQOAEJEGDA/8GrfyQjpQUF+5V/Uuoc2JYAP7IE8DfCWACeWFTdAdn4UHgXNmHX1iPR7gckPvDTsP/bGUnm46o52WGFgkuHNI8skm/Djylqu2bCm3eMScIHIR8lATT2pqfXbX/jNZ+8/NkdDW8YUyuxvdfG1fw0ftj0P6u9a0f/pPM/l293D7u39FNqt+qYrrz1+varb9oXCdE8/jGz59bZNFK0er1tNyw2U70+o7fBVrNlfgvn/QMJPuzjAqJ7xr1l3EShVy9Hfeb852HI805WjxnVCAhGBmmZiLIrn3jmufUXO/0S3TEyosg8TH6yenrrCRL/Ozz2eKE+wEoiL3Q7i7my05h3OSbh8OPXmzwkjt4vSpNwMRRg+EDVNJsCfkiuu4+GnpqmmKQaOMfPAFTmvTmia8iia+7EOT+8X7W9uXn5sHe9Qi+UeG1Nw9RwN9mZoxdALgyfieP44zlc/QGAd8yRTVXMfGsf44/SZxvbb9NyTBe/46bqpUEEAKD01k/21SKHbwYx+5b7wnmT+x6YZ5imAeQyAAUN0PACggEEiH0V+5AOy8AzD5gBAK0gy4nBWxbiAiwyMDbAqzxbbsznlhXjluJIAHhLkhDWAJ9K1IhNjA4A/lcc1FewfOBPgjp2kUSrbAH/wZ9inDp2j+sufu9V7P02DofPe67eI9j9GH7UNEQzRFsiWVKDcNcNEkBz79rt+pD1wJTZld7rlSXVsJuM+FH7uZ9H9/7g2vNfXhr/td3jxb5GHkss+lFyuIy7wqdp7pyi9PDZvuonn7sBuidzYx67WuanZIw/IqvvPOaYppPEC42UDfK2HafJukn+L2krKdb6JAAJ9KtgHMHuoyIpR7/m/uvr88rS9MGyZC0NQPVPT37Gp+YcfsGvi0O+NXs3n3rHDHV61huWh8E9HT/pc9Vp+BxB6j3np6/Dxbh7aXFTM02T5HbA1O/vYC9+Ysd87J0BgM3zOXkov47h6vM7n1nr90jGZA9AcEl5+YqfqzaTlZR7qhYAcAUAAJB7sae8TLk9vTlvZ9zI71Y288358c/6Tc/nGwEAQIMp1GcP3/eB4/YUod/4pV1nZ/Pz8vc3ij4MCAEgobAPQAD0KlFI/njABcVT6nVhzAcMvAYADEbInp9//R9G92WDAbBhr8IY2PH/lIAuAiQEIF6y+EmmwA4A/li0xmsGAyG0iw11tBKJYbkmExD8SYjruD6On++Yo9qHNXj5oru1NJqL9b6u5h2djYiR2aggMz0IT5EUCSAcvi/2Lkd++W/TvOHA3/qDxtW1SZb42g/K/8FqgmOJK2vSVnf/9rJlL/tPfHu3jlxVIoEGk2oulAMw7Eg/p+bw/Kj3A97zj8kZPi5OQMTGO91RODN3xNDZLnKUnfKcZb9Fct8lgDNyc8R98r3U2LmQTO3WZAd73kebvCETVBE1Leductrfseccm+0Y20PmPnYIHWAAQFwwFLu60YkVfWe/z8tbxeRQVAN4+Fyb/0p/K5tOAU3DtsWYMc4NzSYBEgpgf8g7X0v8/feRlk9dEsXkITEA1Rtuu95e1d93DGzZAQ4A/LAADKii84g/evK+u48PXH78u2Tx+d/rXTvvlzQATERRODuo3Nl7xhs0936YO97b+bv5wMAAoZGil2D9AA7YgNBlYQARkf3ExYFBBq3EAAAiBaB+HIEBnwUsg3Hfm20CcWnpM22QVQSgDvUDvkhU0zU5DN7AWyTa6R5A8GNCkne523VxHf5GYB5ygbmOuY5aiNYQLStIQFKSipAkANnJxO1KQvc+YQy+fLnys2z92ZXRhcHYW+uXi7X4w0n5mS301UKz69Qn30se/T8b/mH0U16SVe/sr5f3jIZSYvt8v+iUAaBJXn7/Xl1eZDtvJTbBd+lxWm7o/9BM3c19JucneN8u7hfJR2drRJPANjyUvbEHw7YfZ+vXYFKgK+VesO3Z2+P5L/48ceiirBTQwAwJSx/+BVt/tCr75u89PuZnaqikIenJuBOALKDSe2jYqf7WM5OBP806gFcNMsDZZP3M9/hqSKNNcXPXi/Vmt8b/tM/HAQDp6Z19O8We8zfHr/9azfgL09QABmDu7vrX3Fl7M/1mnnyoAcDBOQCA2TecssN39d9++np5zyBxVsh1OyVnLA/A0zRnXQeYj4m6cqpuonoB9M+Vv98aCpAOEpoAIMqNZ+mB8NW+C1wgAj5DxqwgSZbFLiBoAqSbAcWrGUKFZKI4usyhAAW+SLTHRy5ATP9NSOITifn0TAqi7fAxVdb/cXVIoXP9bf2g662fLkd0UnMdOUWxGqN+rY/oCNGoy8xGu1IygBc6+NTNfx326tZe/MWb0V4/9tB3czM6619+WVcmur9p53fevnWPV5t9x+m27ZHlXUo3TEfc+7EL5+UvPycqmE4AdVzlo7by0T/g8wNl+3fYdFYNTX1f4qQbT0Qq0Pt/s+Z99i6dbgxUZ9IMz1B7uiX7oV3HBPdrR+/9U/lPHI+basVbj2BGxY6zhqK+ybM10X6gc046K6Kmr6zkhaGaAmA6km0j936mK95/f3FcQAoKwC7MxctrTle1qymyaupgYVgS8vvPF7qHaShQCeZ9rnuetfV/z+rUsguBBwCo6b7I/3nZQ29B0TMLAIeFBQCAdJCXOWRWegH/9I/fOaO6J7+/6EkAAdMnk52ZdwJ8c4m7P/e+d+uDsfbDLjUUDVJZbdwrBSgkFAF1098G9+jw6SQUNMRgAEyiCwIrtJDsgQckIyGBp5kOXtMa8luiaklAgQqgAP5IVOszuUUUP7sppTnrVxL55QWZ+MGfgfS/r88duM8orPp6S1u58O2t8HBj7VhXHyHKHBOtw2VQKTQzEwCo2f8lrx2+uN94+Au3g7Xzl1V6WjfwuW6O+v7yyI/mv70f7SUt8w6Vn9/u79rw6EGP8xNayYt9P+fRkJKwld//ZK/3PWeSKZR16rybb2J4kYnNbtn7o43nJWjKy+jQz+zm/ZJ0Hj1uslzVJz6HPP0M2x4xBNvm11vPf21mmtgkk9dn+em3Wu6PYl5EdSdUdrf4RSbnmdyZi067tq/jmZundr49T4qZZwCmAADyM68R70daGpgXSJqHhknsN9eOj6+H52XHAzTDfH9mwPtCa8cJsMdKBADTe30sOf3aca/fTv9dpKyP7JsGuO79B3teWCZjoAEPwICM9+MLz5/565LZx+F4XbDq7RiI00av3022YYARCRBCnneomd7lynBoYLo785qsZKCMoonm3TbQhANAEQfgxUYEAFc/1lIKCgQGgETPgtb1XxB7q7ew5gUBa0ABfkgM/EeSRCZv4AUSc+YjWILgZJz3Od/9/3l53nvG7CHljzsf9uUQEXnZcCkqnpCCH77raNQvM4EwyQQsqYpnBmA/Vlpy1JxziV5nxujnWuT/Ws9tf3exnxh7+/+bvV0kfj+e87L57XDK393qB3LtI01PsVb9NNNPMBkAnaneH6cjH2/1jun9R+mNR1Bk8G31Y0QNs6aqfBevkemM3P5CT8YoyaGouXlnLpHZQyHd0V7eq+aLf2lptrh3ywPgasT1NtPhUl7/klxc8NnvVe6TNFdcZFFQSV3AUEzrZs58aQCqxGeg8iTAM76ei436Lz899zQFgGaKBtogeChgiiFVDwMAsSDDlufcubLLoSfCl5tp64P89zz7W+XFsslqCO9HF2MieIBjEUAa6KGjiZ4v5nrhMzxVwhS8de9ocOuPv22hSJachMrGxnAPCXCgSgIQFDgBJGyZhLSYGkBaVUmDQIAghJgVWQIwIARCEkUkAVggVi8dLGkAcAgA/kjk549sJcTwBfGXSAziD8AP/rDje9zFIOq0m/FDP6R+v0fn8MhDAXPz/kQB1LR+W1qYZAIusQMzAFjsrp0cfy01Bs2Bxg9r/2ry/xqQebo1V5YZf3l/v5w/kXE4z+PcjcV6mutPFlXu9fnQ4ceXi9/tvc4/6tdXbl5cGj/qYG7TbHu9f6pft+w/e38ZaITyhcvpmC9js60rWrO7v8n7+2sfDslEBzlJEuGn2qkDG9N02bF/X3LowsxAVcSFJl4662Fjf3sMm6DInZCd89RU+/Sn3Bu8+Rmm0b2hadrT9g5zJrPczblOxew/4483gL1rZBmdD5APVEGpSaAqmaxJls7sbDjb9O1iIAEomXdb1GPvqZUXz7gJeXuC1ogJBAbkS5pkireiWubCj03gAMBsAbBgqJyMsmdin/ydS9a3COaajj3l3XINC9jfgb6rAURCTo+0zoAgAGMUgIIg7QELA7GRBYoJQTYAAFBITPalFCH5rJLVJECo+KXMrb1ld8wAXkjM+l+Q8eP4A94hMUy/AfOBP4xp310Yh1o8Wt31Q3gBTax+Zr/SzDHG2mo7wiIkM5fwsksC+HHs7/r/n+q7w58c9W8c2r2dvDCv9Y66ZM8406ZPeGPD+5M7myjt1vRiz5uRuLt7ChroMFPsTX993Mu+bHkEJkJ+q7v5CLGBf1u+jEgx3ZVziE49qmZ2S/aBSOnt6+46Z7KgKfaHnc8dSTK5xTF+vO77/Ko8PR3d73v15dbPlh8eejNpewsTFJANzIMX7/JdJO7dkz2HypELkovzAgBAg6mZ/mnlp4fqVOkxB+B957lcs5E9xS2ATd7DYqqgClh7yK66OiIpj0kS5mRNEBmFZsrvl6Xf7q1z2Q7JNNikJu//eepMpknnJPAxzAEjFWzh6QIqO44Z90O+Uz+v5usHd/69D/eQGJChSBN1joHBvBoPwJMEMQihi/wKliJhCMIaMAAysK6ypVVAKAAAwAivskDAYoMsKVhkkBEguD482nZjBcBGAtAaBgpPZ2dTAABAJAEAAAAAAMB7AAAIAAAA2Mwv2hb/f/94/3v/hP+F/3j/ef91/3f/e/9r3khU6y9A/uDfYMNrJHLlGwbzY3oD7/4ziucK1Yejv/TwvGw59k4RP+tjrVv9WpFF18zCJDMpxk6YSQBjm/ew9vNWPc7165OPjxsvfqOKq2pc42L9ndGJ3SdeJmV/un3JGeo3/FlOj3zV6lsDnx6vdmY2fSK2m+Z1iHNuGrgDOk/r/comYubjYlo5lPuDQ277HHMgYm8tA/fMTqueHqhu3FJ27nr5AiRdlUVez/zrfTq63GQ/7M/xsP16MT1zSYmSmqcY+Mnv9IW66fzz/w9D30tbWdnviP0KcQKC7hPw+/SehzsaZvlm4sb64WgadScYqHtK/WRNDpoGYh7jcLfbyox7nAs0u8pAJWtH7+F+315+J0tjnulHx9yrZGAo5UvXAzEct5zF8jYNC2DlgpuQRAEEmdOvKotT6Z6bqmzIy+EuaR462Y0ZDxiggRgY2EMlO1ROoO+jJQMG5i1oQSW95Wh6l/yFZry2B7IAsIkNhYHIsPz4uWVbVBdNRQtQAN5I5NNvMPZh8ATpV0i00w/k6WUm38ERV7/7ed5oKJxA/7wTKl1+R1TJOrr0ptGGhrrMLEYsRhOQJBA2fnenR9vR/Ie1cXL89O8fzEGbxLq/o4MXV9ONRjew0ujA2UE7w9r/XmP71/vb22t+sZvcOocYbddsVGS347nLH83JGmgAuLdOL/UuX59O2/BJymX5UfN9c52xcp8BetiK5733ZH/++mjhondF2ZzJqV8IpiDIoz57ZxeeYukanvtvljfzQ854FcsmLHiZus4queL7Ie2cl2RyTyF63x1nchPwnCJX5XtvN/09PYozeTVYs286zwtwxTIKomoqZ0aY95iatmNgQ9Jwgt0IRvR+fGXWr4zul6e8u4kfoFdwQWGH1KWFvZSuoCXhYhxsq+ZqfJXaiwMEK+p2MjXCMBeXtCJSsZZ0sS7MHWQTGUCJACyuayveSYwCHMAdMRbFCeQzJTLuGwoRSLPU6TSsIH0shYQDFkIb2wA2TAsABL5IDNofyMSN6WdSkv6TRLL/glze7O1vQ/3f4odH/TykGdEP1fX4U7jxqPIIRbDt9X402mZUhGQCAmYxYpIBrL6s5OvNOsemzurpyv/55cZGtOGOr0lnjBwlb66tbdQmX/x0RCv9ydi1ocRl0xj5bPp2V2kg20O1PMY3G0l7tj0SgD1/1zfeKx+2wLxIl6z0+OZT4U7MatREdFNZqjp2rRXv8g+63476Ztz9OUlmpnseN8MLLy/UIVjqx697JfZn/9ppoZ7ZAdgIepLt5E/iecv5edf0Q5H4PIfsb+4/QMdzuhOqGSBlTMUfa85Q1U910tmAUdl4eH9mLOMdP4fqQXWqAUOCD3tYCms6GnKYxoxHgj1MLcmpiNJSaykP6SQRmCZOTTKLfgAQRsi5lqM0Lat8nz4i3d0TtgcVlU3WNDBtd6OSLlsII2McQIXsehOJmgMLSKNfrtedcRoYJJ44S8QrjzxDrErSypOJP6w+EFDkK196YRpO0wFgAN5I5MvmYHmY/GDDt0jk+i8Ysxf8bFbU734fuVw3n1z/w3D4j2rw9T5W05it1iQTEDOxy2IM4HzXeNHvaE0Ie7unxf2fxZhy1uInL+KvLjfMrqbhdrB2f1JtXFSbedm5WW/Z/cv/2Dps/v2xQ28fvvj8B6Do6w81P6pVciIhZqufV7G9sInvy/OP/m/9XO45psnMYYV8dNV/o4Ksq2feuniZEmUgEioTsgAWHSk+c37iu+cjsytb7Qffn5OPeqF7mnMZmztUfePuz/0km5PEqj6bBupPJ5N1DTvcvRGwq5fxrrmuouaZ2rxnbhoAtqdNea65kiza07dR59DY++mQjaA7ZQK9oS2+uPAwH5UnVD5H9zN33I3MOp5hRuuipz31ZDdrVw6mRWuQkbxMUfHxPsVFGWuLRHYVtZrMyUgrTDaGFt2CRTdcap3UPhOxIASShzgRxaBMmpigt2s/JNaQCokSWJvwmUNbyUMODaaJmMAOWABjy+M8oE98n5YSLCkAQ4BlAn5ItOLmYM1L4x928lskKn1zGOwLfiZ47/3uRYTLtfswTz0OfcZ/CnexENcHKIEx6kd4a9tquMw0s8sMACbtdGs5OUCPb9zbRd910y9G70+rvZXf/HSp3+F35bBhyt5a/VTdmNqsYehyQKvkXqsQ0tLiHpmrX3JZei6A80BwzsRbHUcYH2bKU+7P7PxWfF/zyme6odPxDV2d75kz1OxvNsukHw0wQAWt7N5l239O7F14fC3Ol39T+NB7Zi1B1ABNN6I7Tjh9Z8/XYiWfijGQV2dvCoC+ClDtDrajcPj1ksdY+ujWFcvYmIIcwHfQNXUQpxDlShFP51CaspVQwSbBXqImGWUlWtPwkqv13bz675In12Wm8EA1NS71Qs6ijAovTU4hkhCLBkwDOVUu2mixa0hPq1yBqFuzddZh/tFLIJZV2FbQabmLXrVltauSFDaILMkE+aAzbfNe15Qly+nvpv+8MAhkABZtyvfNtK8tvbDjt6LAYIwd2wpTP6WzQEVbno/joQ7eSFSnzQH5gPeB7kskKvEPIB/w3NC+26dnUJmoDynUeGOcyd5nyw/devN55Gns7WhtaKZqwcxMTCxGMwGA5ou7dC2a3HMvEue70bX/o+lrV/cNM/PTCabrnjS6Ehim732fx7IhXM0x6NfW0z+pRstyVRjIQWi2f/86Yp8B4Mozv348zSE94cLsNX3vKMONV7m/C9UCcxaN+kr1kt/VmTm8/QSjZJ7ru3s31Oi2q/Yh3y9tFuZndPW/8q16IZWth5c9swq69tJkpyneO0+/a2bEdd7fpo5z57D1dXCAzIYiw4RRfxb/1NJT5MPvsYU3l4YxNFlZ7nIOr9A3JzMjAx1tCgQQQ9vYpG5z5mr3jLebx13prtnK7EXSc2RLVO37ezG3RbVCJFIlFil7sugMaR1mSPTkntvxuBODENAqB3QBQGhAZfBA7jir/EWynSP9rdSJokT1oLnGIB70+iC9tsA0vnkIDQtg+dc8kCJLpsGGCFsIwdEHAgC+R8yIvwA3+GIj/Q+Jlt8ckDf4m+B/Lw9f3vdG9G2crZXTFRNuUqqOB7nBt2NtI9qsoczMNC3GTAI44ZHkr5xlmmue+Mm1/3rHhJLvag0fp7t03NvjcLz2d5VOrsxcmj9zHhrG/7/jTDOdBJd8T8JOP8UTl2JIgCbiNO+vvi5+V1jT04qKj/7/b10dxbGvbDYz09TOPnXmZ0nVfvbK72t2++xuBmKDvg/U5Un9fk+eoiYfA89/23HrYicODt84G6CpfQ25UVb1W/4mnx7yuvE8VCamQCliU0AB0H17al/FJDMPcJ0EKACS0cZ9c8UUVcRdlQBU2puEi3XFIhYlVR6xgJ2d1qwI65xCqXYIfrKdMhYogDEtOK1dPFJ0iQXDRiDlGQWobHebsXFZGEzWKiAAj1jUwHRdGG2cSG5DyPLTXuBLlXr9Sy8PSLyV1/L0kN5bUWsL6bVsWa14aBEgC5BRkfEASRYCKV43koT7C0iJcLR5B/s7ICAAfkhU418w+rL524zzCol2/QM5L5rvoaS+2336L2TZ/CeWR9w2CyhLEuZobPW11quqFGOm2YGZAEAxfxz+dx41V1+Mr9U//Zcq757d7ceE/RLbbaZGLX9d/Z39/E3e1iW9t32o7drm6eR61yaed0HalowLcYze8vPvIQ9mA0g6D38vH5/HP45x6aEv10PnYYt6wLOcivNaWd9hGB3KVFQVQzFRacr9LZ67u4CspyYtsdPX9pBrZt99YivZuB7nNOAGcLYsfC+9ufZEvWsMq8nTu9nd9dA9RF1U7l1A90obhG1yCahsJVJflU1iyAWXxSkgB/I6RZ90Z1syh4bqhaIzYxcReAaXLRhk3FpiSICqoQsEq2GsUdlSsSDClUCygwgsDUYRTAtawj54Fc32GGD54R1Qq51X2bAiypbnuyQQQRshDhFh/HjAAhBcIRuwQCeInW7MO1bWRjZowQZhrFf+fs9TLZiFgzgFwaREx7H6+k9MQYUBfkhU679gzQ7+vVmR/n3EjPkXcrPTmp9tuyL/7323lL663d7vyJ5Hh7UOnIAbZyqE/1kDP9u1Io02IiqZ2YHFmEkA8b9nfvfxx2W3jTNfB6tnT9f25p66+ro8XVnZZh/Uv88aFzPpUctLb9x+xPW/JgWZDNtWssDE0dQ8P5V1twRNxn3k5v3PuORz/QmzdMrvkLlE0Ah8lwOToK6dvtR+ixbrq7JAeIf/OZWzob8xYoL9Oa1j8k73l35heHsBePVcdEND3/lQDzDP+8PJex9KPdk1+3hlDi++qF/PxF3ujg3m3vHruOvZdjcX9iTpS+kj49MyPwPuBEC8qVNjZ8WFglIiDG6PhR0wEZMm7Ow1g3F6qnAcalQgCNRIAtlhjCuzDa9UMrspsRqHjIHbzHRhZ4Ip2yakoYa+qkhfIyQUF2AMGJCJEIzUhUmswH7v8zGVANZ4FRZxsGvUnAhsDb5KhAgK0TUCuZGj+4QbwG1z+BcJvCQBGACeSFT75jDwgL+bFfUrJNrpX0Bv8Hcg/e7nwaZzxTef/X/5LFYORycEXetna42OmjEzQQq7LhMAWLnWeHY2af+xb1yMfJz9xdlA40V+H3bR3NiYTTa31vYgjc3W8bLvtNCeloAx8vPnu4+uuxnOfQfzHYsowF2a6CMWqwYSUEf5vL/Paf4e24D67ui0P6m3XXDtqWdXKpMeRWgP9/wPFz3fqK75F80/Cxr2nH3rYvJjpzOK6Zn7I9740sf5ssnnlkxAjwAL9FaNTt4+m45Oift+Pjet5yTq6f0d+F1FDoC66JOR3qem05Ppxu67zdNY465EWVScrvILBYDVDMhFwTR4Qci9qgYw0Hskz20TIzCCIgLZDistIhRNJqnPGYJOEIgBKKYoYHUcICFBQywTM+oB0Va09EhzCieAJiIyXoVEejuVxAwAIBCSTZbIH75vlAEarmudAryCgFFFawSB94cFeYzFqpMEEjaKRCD6CgCAAMSiDp8O/gADBgA+SLTi5pDJh8n7hvoeiWjaHDL5gJ8b8vusFWN8vnOcsy7v+j37L98buqGvvFgF5NtoW2tENYKZmYkdmAHgjM9tl7rm3gHCvrMZ0w6Xdkuyxzh3Jy8O92ei+pcjfTOiAYuenr/w6Dcfm32x2QzmIiJky74ThL/P48wCSrBczN3fT9KtJnPgW3b9ifvj3yagu/fL8XgIABmDTO+5Pz5zrr2bVFHt3fPJqbpgys5co7qyOVRlks8s0ffzcyf8rRUyqoLc7syopkN64roqDgdPZPJtdtGQETX9s2+cxkxBX5rNbu4tVz57/pFcOkUd1FSSiZKKrqqBoFMg4yjLriEBSqsEVYSZKWNxU9bKrBsY06oIDEwSitJalyalqKQGNDbGGASRyn2ObGWh8QxEZZSRYAAmMcioo/AGIqTRSAiMiDfZkpMXqufEMa86I0CYZLzpx22wyUTNQXoOQEamMaGpOP5hkKLs/wJ/ygkAA09nZ1MAAEBUAQAAAAAAwHsAAAkAAACVRkFbGP9s/3b/a/91/2T/dv9e/2P/XP9z/3z/c55I5NvmkHjAzxPyGyTa8V9APODfE7zbYZ8Ob/U+T3xcwIXTKOiyc4xarUPVJJPssiNiBoCT3vNHK5fjaWNq9SrEbTKx0Q79Tl5n+78to3Yyb9K2My+bM59cbJt/+82+ux+6j1/nzaqz5rmvg4/Mhxn6Fe3uoGsnk4De86uSXj4//CYW0OnDtP2Yh175q1/9/TuUekiob/Csy1rMLT8HErq3BoDZ+1OmH6nMnQyjxmTwFnHNmGgfl9iaSZEkd1Er1I7tlXK1XaOelsPOxJM32W+fzH4V2yQ3JpEzK/uiv+ot5QwBs8d7WLnDeMgGWiimGMVrI9AIIBFjiIpSuZ3KxhWEgFpoBqqR8RpVmxiYNihU6EiRlpTFQG1W9YAiAuiQwQIIeQ0AWvPdiNIDll4h2a4/mprqU0NagtIC3XuAUWQM2fCE94IXRI2O0KtlLLEssB0TAtaAg1X3Fn92i+2rKwQAAAB4Gz60YRCAAJ5IJNtfyPQF7xvqGySq5S9YfyF0s+F7u3stc501K6Jp7POC+oz/kNKloIgyGaOjVauvEw3JJIuxS8wAQBK7Gf+ytrdg16OtTW93UTPT9cTvSXZpc/Rl+JUrG3/iXOtS25eXb750Kytr757mXfU+Uq0CbbL3Od/aUuHAtNksl6+5bp9vl6S5PE6/Y68vvZ56N5de5OTssOwvx0YOcbkufV+npuzT2cX7xDxk7kry682/9kS1l1FYV0SqPijyb48o4vOQW+dwqFxK5H/6aKg6q9b08+6trL/9Xrmp9SeXBCaB6wjYd6qi+x9r5ehCtDFpWFR2zrt9cVehIucArAgZWxJNQQYtGgJoWFc7YFCZAnA8lEihCYAWILEWJEG8hm4kUw4NNLYZIbKgZIAFANJn2D1LG8bxvYGXjbYIIMCWLbLSM8ohNIJJoMfv04/xtS4coTlf7ByotJVDrbnY//amcJGe9bEFCGQb4p9X32Rz3rIlOkykAgr+RwzKvzDqB7435HdIVNt/kPuH7f8b3PfIT8K5ZeRycX+e9a9RsHe066hVpEGb6zKzywI0kwCin9H/wJo/Bc6/ytcaIuf48YXtRuOhL83HZXtorOQL/XZzzj2LhV397vnnL+/kqxeL8mTaplyTWXBnTjDdd3fdCZAVjB6lf3lA9r9loR43NvnO3DDnV1zoVk7X7crMEf+W99lcDUXrCHoaqv27PC/3T9bUziwrdfnC0JO/Dh666jI5LsEgetfkNeQfXLOZpqXuN8nJ0Todc0f7YaCSArfYVZvq1lkrnoTZKHRThlxUkF5cWaioCYpiDAykMSCcMRDLPDQqk0mqDSJiEZho3RAGQsioRSkNlEFRCbBvQKtsZAFj04IeEI5wo6Hk1lSrBMCiAFkIB4VgYPrlXw2ItgUIF8t9fWl/yWENAzngEiwNZGNAZsGe70+dNLNfywbN/Hrf1RgQEAwC2KuFkVaMhCwEkCMkAJ5IRPE35P6yeR6sXyDRin/B1gd+nuDdx//LUjjRuxK7X9sxsraiVoWQzOzSxDQDwMVT8/q1q+TU1PlX76Bx2kEO0rf+rqR222pvwHI2qXuHx7evD9v6zd/sbn+BV5xf++M37zyRL5c7HwkZ0Cp3LueQB773F/KFZszU/L+1xmLlgsfkRcm/nibXD8/o35qsFfVKXXerkwu+86dYTxfPAFftYqbBmT8MmXnV0Cw8V7xcq/J0kLmPv/YucnKxZeIPhzlJqUrHEJn7PftefxLWhFnOM6bO9RWGZBBcCH1T9I0Gjm1IIpeLtClIrfM4bryIXAyJlhXB0AnOxOvYwqURcjirOy0kEAgSY09gCC+O2gZkRKnSInpaAAxebDAwgREW0B5s2UZaWkJIC5KEgDbzIlES0sSqhFI9Q3dLV7uImjF1iCoEzE7jcrz2OYn/mB34wJJtgjDYxhC7ZDvFBGAEwkSgJ7tFTzrGAEhriMCYwAbwaCBDAj5ItNy/IONj8O8J7h/RcpuD9Y/C35sV7n39ZXJproPfFBxdNpXFS9e2bX1ozSjJzC6xywwA49B80Xew+ZcOXOaTIzl1utXO7nX29mHbq72cTc9jk4v99X6rr5u/xvTsw+Yl+599JDd/ULzcTlV81WDDfXXtq/9DFbCp+uGkP5sfUq2uaT71/KWJU1VUKjwXmbM3ja+tYenvyXmaVHbSvZOKepos6GQo/QTk3YfJvCf8nHM0PT/TypoIgBo01Cyu7FTVtfPJ9ez8AmsNa80NqiRFqOjMVEwqmaaEMtM180AtsRFQRshDmpJE9+ixTBFOAxAZwomRWWygosJaoZUWssJhsds5gJ7TZb8oAYsl1BR4oC20yiowGQoiDYUAUrHaWAYl3ogHC+IuMKid73suILndEoboSUV7P98H/M3OEPMdZyIxI5K8ZYCQiEAgfEkVXsBXJTAALBARScL8URSUZW1A4BLeR7TMv5DFx+bvyR6vkKj4/yD5x+D5hPruwpkvUdB7dj4dXMT9F+haP2pta0FVMjGnImZmAPg144fvvTReX9g+7T8kXzwcbZ5fDRz78Xkv6j+NfDbTzbrpo3Bu1381uwyju5dWb7F/aXWMP76ZZ5i0MgGWztZnPOdhKKD+1RbvSf7QKvMklJ/ahx+3D8Q59KbemaXWL88wc/fMMqb4ieQeFoo6CUnV6a7vW6ZEwj19+Hj/kneRXKYUNX4XIdnNCPpnhs0ZTs98p4D+V/1e9qIY6FyYqzwTUGcDuZ/Zp9YR67Is3T25xJmWaoAFvFZiEKQS1xpTCJkSNOCS0gVKWCLJkCI9YTkbnjEETewilm00eI0NmE46CqRCJnRmNA8MtYYFEAlTaUKAJhRSIROJJlyObU72JPdZ5gEjawCCwgoBYCxAiObwDiRDMzcuJVepwj2FGCZcpD58GVsEBLGgpzGrCpgDFyHNtXlsUz8NBYbZcjNvIMABnkfMiM3B+Mvm3w8b+dsjWvoP5Poy+Puy43vH7xpe9NzdNvr20OFL83Q5Fstj0FHrMI0sGnQlk1wWY5IE8Hv/v7tR75+t1O7btzlM/zVqczp79vZjNFcOvgcf9uZPv+ucO/Zjnx7/OfvTyjc9UXK8AoVg9RxqCzVF0SSZuoLn+6T4d8sgx3kWx5NcQsfuGRiptCMwATfVLeKLfqa09MYBvu3YDXyLPQVNyitxZ+jQO2MyIqNCZE1PHrwKLqTfInH28kzmsD1N5jP1jyqHPfaZRFfRDdyzvjFJKt/pynFTmQ3rR5LYdrwaiy4Ric2ZRvR0ga0O2WDnrnkwv2zJIaWmSaNsXpjFYElhGUlKUQ0KgRImDBUJYYMQOAr324tXUDAhAzAAHcmqMhIC2wYogIAAxLZMIKPJrfo4GaGUgkHDMpAAh6HNQiAECLFaBoAEiD1ZWLc+RygKgPNrLgIcAP5HVNxfyPxj83xCfn/ETPwnZP3H8O9NRb63Pd9QOF4nOKFhb+vbYTVVcVraJWDxBC6TAHB2+nB9Yv2Xo36c71n5Pjr9MT63fB+uvR4uzt12T9i+tPPT6+ULP7D/4iFfvPXNJdn97qdvng3+IlN+s/AvZQSk7+VnV26gqZ1f/yJ/mV/zFLy/Ms+1r1Xu4eWmVnWUwM4IDrkXn7ErKxeenfX52k0nCbSpPX/WbVKmj1Pvvce2P568foLyUc7QVWCGptvJLSb9E5cmj3sn2H3EpjpVa2eCTwELpbJQUrxIrV5owFvE0moAVoeDc5QhMl7bSDZAuZCiCsF0R5pR4fEl2XIAgG1JYBuVVoqsdmmwb64iFTsWQvOAZkg2s19TIVNQUBYgyrZBoEiCIjAEABMWAsCCBq1PPI8NFgBgmxWEwQDQU0/vCIROs4+zXJtQOSyCMAIDyKaLohgkFBts2MoRgH1BAv5HVMxvsP6x+f6wk28f0fJ/IcVH4fuH0b0L8ylX8dhZcyl47n6t7wiRacPUiWRmYhZjJgDwmy+efa6Er+iFn3y9ywfn9rbx/X+1GzbzQ+J8X3i72diNNm//kX9JfneHW+fpcbGX22/5r99ftltNoqeZq+EFN092nlEgYCA4HPL7fmz3qOpweb1N0krq8+Y9pFwzGm0cvTUfRX7uItsFeT/vZLlBtGqqYLjEeY2Uw4aQqXyVh5mQn7oUkikvEIF2UUgGvME2fclC3uqXCU3NzSTFDdA8ZgporYwKN2HaniVv4ACoFLIah4QYeB4hj0clFTVggoU8LZ2uq7cW1KwMSaRGFIawLZyAahEYK5SQobCUi9iXUAQIaUrQTikgaLCIAFUTaRY3GCQFFimikkBgnbWBCkaS7ju7FHqs+6CgBvtVwFrCf7A6RuKqHi2T2rgX5PwKQc2vCcWQhgAAPjgky1/I4gXvmxV19xED908w/WXz8yYHuZ/x1Xp/73nbno2+V+fgy9u7Yq99fs7hL5wO+LVta9HWtJKZPTEzCQAV75/XpcXcsos/Jcuf+vdV/4PR+HX+enRWejUfFt53h5Nb37FMDjj+F+PPbh7X36fh6e/vqnjJFw6yR0J3UG9fuxcrI5sl1cTgvfLYk7mQbi44c+htTrL79+Qc59tPtb65fJvp2XWS7F00s2x1Vccn1/lhmF6yf+YJv1u/DnHUmaPKBDHWWFVL1fLhc4d5PX++o2pnp0qZDUR2lVZBTVAQJSwkohkxFG5gqlz1Q0RPgC3GQAp7nQHodHKG2qyIVVgQAQKhBEQIREZRpVVyYwDbnQCWNQ4wSxWyjAlwGykIESOBlGwjYROAbBBUwoTcKkdEp0oYowNEQGEJXMbCgKOiEkwIwzTy6qbcN+8naEGCSkBREABNKCOhALA1RkAM0ATgx+L0+vSf720CmqRSp8FLAN43VMs/MxSCf08wDQ7V+r8jG4K/J2NM7z3Gii/X3s39Pni4yP/0UaocGoBf2xhZNNpGuMxcymUSAC73kmz92s+NOdXws5LOH1o+9/YvLLb1o5engwc54eK9sWd2BLbt4nBC+pfDv/rBPz//+OJj5tI8m+EbujnzTtudAzRy3mKLo7FQmXtz/nOXwzJ/vK5698w58n+a7uY88+Vaa4/qZwZQz7lyoPNUQ80kxC+964hi/v0w57vfD6r2fhc+7bi7pz6WeX1tzD4y7u95nkO9vU3ULqXFFFc5yrHMfCsDNBPU+50dP2rTTgCisiCtK16tNGXcQPLvpiGtLAfpbkaiGplee4CBrH5xH/nlmHaJWM04CEViMY4MFkaM0CrI0iBgFQUMRPFonHIFhjYAiTuKETXCqRjawDQ0Z0WdmovIdAEqyxhvmHR+ZikUHuEEEBjnE8F9W/MNvAHAJrfpyjDyEuoWBsXSijBgoVHeCSoRAyM7YhUW0HeekKCABQo+aFTa/3MU2wK8n4BBI2j/J4M6eP4Ee2Lu/4wjOPUfmKhPc3EVC8XbHBV11DJR1BrSBeUSLggAwGLibo7Y4+x/9P468o8beTOu9+LaaH5tbXuNw5pryBK3+XTV/fZfdbMzV7tzv5Ff3Pby9+S0Y9QktQQmUyL3yqGPe17YgSbj0L/qez+K29OzTEeE+ljWLJhvdffTtKA39ymailfQhz6uCzN1+hucWZ+H3x/J7Mm47999nPMLXj8x798fGEHlfDor99m1xzl5PED2txwFPq07p66cxP3r5gfebEpPAec/vSDnZt/V/jPMNmO4XMhTCbP3MEzHhOQ5Z5qehsCvgHomx/F3ToFv9KrphDbMCJHABWuWh2rz34cl5I2g59P72sWcPQXo3DAMubgdB29d3wVUnpN0/mlIANYCGoCL6eqGVFUyVUVdp3iSMcCr33xBjDXDQ1OTB0QBgt0/DhTA8xQaALgwpMwtInkBDAiQeNAKwj8ST2dnUwAAwIABAAAAAADAewAACgAAAMtJAW4d/3T/e/9o/2f/aP9uS1FUUUZDWFRT/4P/gv+Q/4L+Z7Ti/3OUVA7eT+j5M/Lp/ynJHfw9WRFz/78TCwXb+JVHLuIYlB2rb9tqG626zKoSAICQ+OKsXVlJ+H37Ppt9DK9zy2Gxfd2cP7Zmd3D+2mqbnH645v8bnlrpojT0TZ7+5dn3Vz86LnZazh+BRCcz88fBqIZKgExnFM68FId79u7L5XnjVYSnydZ1H+qBbzuFBzHPW/k4bD3Z+IpB9DH8UMlp/FSq+7hs8wnTaV6KVznp/wEr3XpxA2SfqE6ewixTleVnGrinz2iirFOnQVszZi0l+XTVOx1dqJ7yyUe5bbqlDwBdDfVlBgpwMX2q8r1QqxN7pmUa04sfu0/cKLRZ6EfIBdeQmMPtQQ2cDw2G8pluDNPKqT/SnNKSidVMAUmmEijHVQJv8BUMnnMNEmQSDwBJr0ULBB+YqRYmOnEIKS4AGEgYqhbjbLEiHupCSIwBgwC/HgRQ/gNwAQcpu3X5qx+mUzRJAQ6E4OgwklVu6KAyAP5ntP3/c0oqZww/X+j4Myrp/50UDv69AZ8m81N3Y+TMxZmRI3q/+GG+zs9mSx+ay/9wp5tnWGf9qFm0bVQyyeVgAMAfTU9O1cm2otPOONjvdHXvkwPmvfXu++L78mLY9ihcJJwYqw/tl1sft3gs+fly6o2jAaW4+uKnmJQ5lTCQ8eZUqecvRLL5PeUnMR6I4ZWDLx07azF8K6ErFT6dUHp5PnTCCGCguyZLv/5mw2aYfJ5LymX2Y5GH+Z3BkgNJMmdT/TVTU+tVdTheYe6O7+m9UfNWD2Pc+xg62Zt6fNdbPWDAYz6wy1OX2GnIYuhqiBQ1o6bhXbJl+0x2g1trg6gZFaS7FiZhYSqCnPPiLqYrrzadbaLMqaVNUA47Meu43YoUVWcRZ2IvSS6AmTSrCHkmu1uJSxOXsWgMw9Xn3lC8oZOIpgChYuRe7QHD1Mu89A6kRSeVDLP4OwEk5sCQsZCJhY3AvGDBapRFxIyfsW9eNZYEE0mIAA4DAN5nVP3/n1TO5vsDMXtGy/+/gzr4+weZ++6jo+tBdeni2vE/kBO7rr6tWS1CQ5nkMjMA4Nb5U8vGW+np5PTXQy8uT33/j9Z7/xeL0f+ZRBPdnznbLKtXDxt2e7fyP2d+6wt1/vx/nOrgZftQM2TRSV3J3//5jq+E6j3h7mHYbJbLyZUL8mj5eqY2MOTP02W66VienInu08Nn5NAXxBzG0/UWDAX9vlfxGbmYjI+pb7npt5lsz1VpFwUCpmbTeooZrVdf0X9Nej3nZAkqa2bgG0/9a9FzgXKcpsY0L1QqD30b6c2ya/Vn2uYmbufMnit8dRj1wPiiGZZauqJkFkcDAqoAANNCMeXeP6YYN1zDYsaVabxhcAM0hnWpYDkx08BCdoWAps2mj934ivSg9A7Hs+VzD6YHwRAxckpYD63Hw/JkO4CGnb+4FhSQMurUOwEGGiC2BXjWRQYbA5dqzmrVst3zB5CA1AYAPmhUxv/nlM1UbP9/KII/Ixf/38lCKbx/oHIfP3LqUTdZvWe7OVBwIU7/u8Zo12HWtqEhmYDFmBkA4Bt+5xq88eV74uO4cWXmvT9x58b9yve8zf945m/1dNzK5ETnP6b/Zuqu+rry//z8j/Mz7PZOCKlbKgnCXTSS/e9jEpuE6ot7v9f52H5S7uAsPjyv/VZvN97TOZlds9/tOnPPc36PN/Qu1c80D7pnJ1sMLr+ewdb7Xfdk5gzRdnozrrKI8ABm7yoxeXJ7vjR3XPJw4PcPmZ094GXr8j2HnXcRUL9rJ9X0dOdPFhjccPV7CQ61sHm2Z3ppgFnPIDlRUgCQNWItk3Es1C6AQYJ4aYARQE8qg02PwTSDsapmKLHCQpYRvZomtBwItylBihiTESJAckF9ZE8n7Nac6vEGddW9h6QwdiLCLhx9Hu0DGfDoPeCRo5LzJgKgTi33ZeIgvtKOUqhSMHv2AIkEAn5ntMz/U0w5+PsCAo1c/38nL8fw/IPGfYL+16hdH/Ge4PS6F+xja8eIRq2qJplk8TSTAIDB/asXjnbXmi/Lx9XLu4aP/VHU17j/uNn7tST+t42jP7r8/M7P589Yn1q7efbznp3xWz7149/dTMkXb2Y20gliIvjcZc7F5cAuJoEZp+r69/GzfoDZzTn1bfLBJD2X80Zckrkpmk/mnpn/XTlnMN9nq3mrInfCFLSoqrU6mbd7Kt2z3v3j++1wkJKFSQdTAzzDmfUU3Z1IyuFZKhv04fzM1lKlnq6eKeF+dqLevUlfP1N3F7AGCZgWSRPPqGKWdsGo1PENkgAomHR7CcflNMUjyoxXJwMCJAhLPtZaCwDQZJAaQKm45R7iZIDVhkKIwA7duhvFQZj3pUK9fTQh7N1AHijTNo2aBnAMAET9ZCRAtBw/e0HLrF5rBNYLMgWiZYoIMIjFNgBADwAQkogi6TLRAaAPlkbMyn9C1jp4Pgtk9GvEvPs/YGcH33dBzuh/AADv54ri/fHV9xnPS+4j35Z4b2Bs7Rq1OolQdSVJQOBAkkwSANgv0zjj1Zgd3Y4W4uiM/9+tza+LnzvXev6YjJPTiXzmGW806zejvpVyHoXxXdGygtu/Lr3idTJm/X+nv6U69c4bM232lANfFxPHMSdhOpqbE7GM+6s23SfpoL++d45CZEVSmLyBLq7yPrNZ+dJVSus+NH1nJp309HVM4Tp5YAiXeSai8kM/E71EJ6IkyOniaUo7bDKHmup3IB6H6yPt1OUmCdKClIqCaJE6FUmBpEaZAGuj1bFt2wDMQCYz2F5t2wACaohd65Rjh9gGA2PG3v29HcZrbLtA3QrDMJQ6sAQLErkgKUU8QII8AHho1XDCMAxDCWMzDMEOwzAM43C1bVAgJQzDwDBcyQFeoBRIK0BSOm9ubm5uUko3RVoJQGDFSiuttJLXu8wwwkorZQC06YruyngkbBrwOrXpim5kOoyRoaxG5gcgeGBMBv6SDp3pr0Rcx404fZJ7X//IDWFVuwuxCHW1VrMEAo8MLNiARdFKNGao4+4qbQPU6YoqOIaeQBnTXe2S/dlmionKBxCukZGTgl5ygAtInKu1cY2Ssdnqg/6JRlFPHBrdFH9GodRqLUEkgYS1OrUrI4qCVa3q/c1z7b73Pu83OwC87XZKpUz7LCCM13Y7mLF1excxRPhsF5M0aYmsJScnJVff/2Gw4Fx6XciKgl1qsoIqikdQtA2aWjvrUFFRLKCmDaaJqmi0iNGM4zD1+g7/hYZ4yQCs9XY9gcuDiptz43rlnmC+gMwyRuncD7AFOTkp4MM4IH7Cxqvabn+Ayq6sQaeCWlpCWSkqaEAVNDN2tV4hCyoSRiKwAy0CYndXCXD5k8jwVHjc+Qo+iH7s7Z8bA9t+O76F+HtufnxiPADWwilG4PM6wGxX7e8b1qGyae7PE8uLIR3UKMRosQZAFWcuo0kQttWoE7R8IlkDtAELP8Eyyhs30zpg8T8LSVtaEK4fwIZKjEA5HTj506WvYnyB0Sj9S7bPTXunHIoOgUABarHK6hDsy3woIZCgYUTbCpQJvfd37eVy+CRpiY9vwoL3jU373VfQlfj4AiycIEfgyXuCV255Ry446VHa/egv7zy7Wd4RKCs9VxG7UlHD0KE7bXSKRRQFjZRkiAygQUSEZzHhM0nBTgGsAYtejJyfDaULkzSg52Igft5KDaMXYIJ6CCDw3QOrsQUn60zq+3mVbLn7MUPelbatqqgrUUUVTc7uNFisqFZUBVRFZ2svMw4QACDaFMeJWI3JbALEBf1f4AuSL6fIvwWLfoHLc3lLG2H/AoRAwaRFYNnLABM97yle3exbe/rKx967SCVeAgV6VkUd9FZ1SeTDDgaHIEgFFcYigNss9fw8i4hVLbJLABo3zJSvZFLBvzCokklS3DBMn32UBv6HQav0BwAAALioDllyzrYWVovQtS5Lyg0nEgBgj/d5TGCc7Ox5uKWDZm3SqC/zBz/vlU48vm6miFct/uSMnc1phcbNGb6MjJjm+/EPPlbHN1++xk2TIr550GEe+veaLc8+vPvby+LZUrfLGaa7E6K8HtMfKRBA98fnu4PC70e/BJIe8r7CpYhVBiJ8FL57aYa7UBZdaMBVGau9rFYJxg0idIBx2FEXNiqrq2IFKFBobDAyVOQ18GCWdW48UlImCAhTgUMYKRRGXYASynjx1mAU/3x27gLrHjNjLMVdrKsKubuaBSajBJh1BQAyoc5X+pABCsBF3Xcx77v3FQWBcgaAUW+WhbyCpQuqCsbDwwPggITvPOfn9xDgx9dff30/Tdfn91ciAACgz2mAjKJ813UA+mlmoIqZyYTYhqoCK2AAcBZVpSEVkOYRQlTsASeyTbQTXs9/AAhsQwxkCN09McSyEJaECU8C3SUQGkQTnkcM0jNJCSfUCqlNcoFDvr5ilSG8gSLV76aPn+L+QUTXyuac729Kl+exTnS0o4nXjszWKcGEBp05xgDQyrprDPz7n/P+OP0ztj/2/2uu3gQDBzunNob2ovnf263+r/yavb6T7T9n+MPOZqHT+XQhyQYAXg79++9lfYBKYCqK7/cJg/3laLAc9LP8+uhux0ze/HSZuDO7r6pG7LMkUQ4T1/pPIN+edS3JbU0DIUMKkq4htVz+ik5VIEpDg0CK28glHyrI4lPFSnSXgD51qaFadmCcLEWCEideG//YjYtXXz1G0MWfaYDvBgD6F4nZtuZnXhanI2j/gIReCsiBbgMGQd7sPObCO3/kY/v4u+9NAYCqlvkkDQndLHu+ggHwABYAaKha8jSt/4xOOl15lNaOn+jvtp+u0zQAAFsKMBwj7nkMQOGb739Dydb5Dy/kOjIPKBgkvgH46gae9wQh5fAq6AuQYAQABtgugBY8+5nnKb8BYAOAylW/BoHzYgAwRN0dAN43VPwtWRBnhlqySerc5g15f0lSgmBKrPe+jufq2qOvrs54G7pywcvdtV0OazRbilZLISnCXUIhyACwPGr+rK++bc8/xheO6xd7B+trUXvM+0lYqfR+sOW05meqw4v4l0MTX7u4r8fa3u/LzbOTHr4wP58L2lRN8S59HB3SkNPf7O/P1NKQ2i4DvVL1+PcS/sR39z5nGOPXTPv65ImSm6dX8llzJkZ9agO1tjECwmUSO51jqickhqe8t8RwXWlxEHfYe/+Ee9mVkepyt7ywUZCT3rf2m/mCZp19t6AQXbuX7p/iZiqVVYaYBwPJwKYHZtO8c59/1j4LM11w3AUEtmmKwjRvdVWjKuiosqpOzRYfsmy7P3+pdW4wK7x1/JM6cbc/6Lwsh65LFmewDw8AGLY5fv595ivy+fv194+9TNOzz9/7t7vOxfMENDsBgBSz/Czl93H4pi/6/mlPPf4qOhMyQGATCgIAAeBkQwMAgJ8tQCUPL79ze9oAjWjm3n/CcwOCAhXDOoDiKEAlMlAoEJ43VOMjRjD4EdAXUe8NlXgPCIROQFpw9fGAgtNqrJ79Z+XtHtE7Lsq38BYlw48xzMJGiMaQBJRuhKsEAFzM72+u2NXXvez5ZbocyDPd93jii6aPKotY+WjWGXFx/29cXvuYNLeWh353MPdv1bxjd/5qaE42x9H9ySl9oACokHIdPtTHXJcLO1C8HY8vx1kAzlXwe4qcZgA0w6z3eg4mXG/JCJl97BkAHBuFVItmMEqeW6r5Hs3ia/IS0wvTANQW1DWw9fyGA1s68kBW33ObjLpgshx+e9pO7Wdx9VYUoRvgYzC9gDnVmwZIbjbJUlUMQDXYPyCD5tW2zvRk0AB04XefPvKE/OE4//rXx8vE0BsAMn3qh9e5sLrnJDDjAQDwACSAN+cF/l2YRYhc3ubi0xaHL+760QYAgNlhS3ZHZr98b+n9P//oPFsbT51vrJ/7GMMoAMUAkMABgHEir6AVgKKLVF5z1JTl/i9ysAAAICwAkH88WmXTgSQg+RWLa6kQCgBPZ2dTAADAsAEAAAAAAMB7AAALAAAAYjl9nhj/h/98/3n/cf9u/2T/Zf9t/2v/X/9m/2y+NyTbLZfS0HxHoky1yKO9IRlvyVEGf6OQ7BKU7/dz3723u1h8+M6H5dXZns/6bJiFDccIjyozVa4gAOBs77/bSEwOsNk7j/oHr72ZF2+Xx/882F1NzCYkrG3n+IzU7va/e3fnu5cuz/7y5Obzhz5k/O5PTB9CpXKB7MzPYPiaj3LLARpcx+NNHNZ7r0nv5H6J5+lf/9eyywBR58Lkhnm5KiczmrsTdud5/nTN54k/WY/LqokgIxghBiyj/0DgMXe1Jn2keyPiGsj5d+qKfdsnmP378J2RBBfriGLmTbk/7tj88c6bQp9P/fwzjfK/Ify3a3jwYb5Fz5x58vnQ1Gf2aeiczb25YPlea3PKBhhHAjV34qzzPlPh6f6aPe8XwyQ4AaihhL7UcmJoBugEGExCFZ+4g28/OvdeODqfXKNtqwDIpzM5U5zzoShx9nJ0V+c6XPirA8cAAGUAkkazAAb9ZYlDTyqsVtyyr9hCnGfABgEARvn8n6aFPwaQwM0wgEA3Rn1DYQNeN7TkRxAG/rWCtCSGPyOs92S6FH5askJIfvf+8sOwtrHOI8c5757+qlzHo9/nCpWf9a3XsOEY4aNKMg/KFQQAdOcavX+jX3asgfu7aes0tDp7+Gs+JK4615b31f5FriyJ3/UOPlin8lH8uqfrtW6SxbbnE6I2Gj45/ZBdTkMHHftRUhvYtyT3xxz8zkrtG2COEPs3OInb+k7RVDQJ/HS1XJ291HcYAKBfKRzYA/1YfJC3yLd9/3vWIl9NRQIAdf6p+2iLyd+1T/266PzHe2fvy7tbdch5SLom7yoMV4Z7211/5yEy7pvTASYGztfX5c50/UgWc5EnBxiY+6kTwM7F3WH6Pt9pANiejfj23Zfc/3yJRQ0HskEDkDT6CiU6y8mCnAYA5tDVB+9VNn3Py3KQ+JTyo3/Q/SkSAACASsrcPRo4yyf133VVvofe/JTf/zcxPACgCMABVIJkTo4LtlDjUq83ALcNIwAAjNWJ7v7/+zRwcwMGsEwA4LUAfmfk/CsZS+PfblamkA00wvZOioVgSFYI2Vu3w3/seHLqLZxk7X7Ur6soQkdYVskUVcIDAID8/p3s+234aFcnnmb/+yY3Nt4S/2Ztg3mXz9JBib73q529ieVlzeHMXefp3ZvtoUTfI+ahDIvtLZp/3JCbnhpS0jJVN2RCHM1rzfHj3r6L/ZDT8/m2b03/JrN05jwTUgMTX2ky8eSL6F08uZ7eQ0oUMJVsi4nOEg4OrU6OsN8sWz/ks8bGRZZ4Ug1z785VVRvH89PrRGiYlj7r81ZH2U4bge0F6yv3Ro5T4Of9i84nkx52MgD4gXf//nJbv7IG1wgaYFmM4d8vPoMX+DMw44bLfflvxM8yH7pJi3cSAOiNv37Y9zevBg4UOQAABgAAFD3dQ3fecznmfGx/f3wQ3r+P+8FND7wIgOmuGWpOdx92Xcd5su6Z59/vzzWAMYAJhBkAj1QAoTsm1Rj5c0fZTpBotOIUAAgEgL0qtgSKsDCDcQ8SGl5nVMxXslimkAIhl2dE5hXMgp/ZjKshfPtW4RCeKetjkdtQOBx/iZI5RpejGqJlWFZBZsoJS0EAQN/bDc/Frs2u3XC/9nVG3cbt/UTSvw3FxT2+vs6bWx8tfo8O++0G9v9fG6JRuFy/mL39aDr9ezgqbec29yQms+B1qVnCRzJhIqK44csffL6IxfPm5/woYt7GlTBsmp3Afl+OfZRyvixvjk0dOAkO9jlZSfKpe6eummGvbS3b9O6bueSzgVhUkBYAsau//XPzvF2bv7LfGfucOVt5dUExqsgFzkqAovbOhv3U7dyjU+R2joFxqLFYX1xb91BNQTJiegEY3L/24QGw9DQzvwJNGf683Rbm81nwOMByX+4nmaani6wBawDAAQAAZDqBV/uw5ZbusxST3XmaCuKBlzSsCE3eag+lvNJbTFHLxdWV2juABNS8rncRMmj6iorQ4sIuESbGpCwkP3kxnLjo+et/xtGciwgA6aEB/mfE6SN4gz/DhsB8Z+T0VzKWM/0fw3gqh9B7H8/D4Hg8qVJw4RXMDr+2oc2U8GGSmXJZAgCwuOrxfq3+6kBjL06SS3N02Py5cbjuYOLSpty3u0WqR2E0tvXnLz+axvnZZKKP0v17Xr+V/nL3lqiNHmEhtwIu/WN7LS/LJRMi81LQ78+Jc+cF+aOf7eunBrp3Zp4I6IvkSKQI359bL3dpfqjcRgXch6KZh6o/ffLIlMwCvh6HT6HfzMs/x1TPWsM7nmToM3MLmkzmDCdHvxbo5GBx1bCnsngA0lAA01l8ss/ygTgXPp/URSWFEhLsYjPo5Z+7+hlvrJnCWNg02UV9fk7DNAxQoBIzLDHPWSvfWYg9JUlb4P7SFPQlwR07cFgAAABIN0nFvfS8+PM3NylCV36TBqCBuTRPZnXv+3o19JXU25nD5JrPAw7S0AAzSMAuJYXugvccBDJCK5iFMSTcKoMsXXPuVsZVS0uyuAPeZwTuO1Dg2xrp1GnPqKa/oQohpCjJrvfx65k4lIIflRtVfSjvC5hjjDWsYZljNkI6BhZPMQAAu6/34V/sJu6PVtei76/E2s+69u/Gqd35w7Lbuvlq5cH5Oha7177vg4NeNE6YD67xkL+XG1fNaca+HLqTXe55++Tl4jRBgmTKAfl6zo/cHxNN/5z950R1VQ2S9e4aLsC8ZSSNJOA4SY/2xuO4l42WbQu2mV9WVk6bTkeiOMUX4H9pjqdlLIIWAMV4vlT1M9/J/x5Ra8+vqsX8MNfWFZ3nHvYATAe4noqYAT1gZjYAJBS5D8M7M8XpfjgC4EDaMwMnt4xhOTdqAADTnfGOrped2qf6+H1ncQu7Awh4lp0s3dMVAQMcAACWt+tU/uvzlP5mi7043pvQcXf9pwegACcFyVU4o9TQPrwSL+9Q76cgUQAZDMQAgEJ4G7AfGYcjGwPYBoBKURHs7RN79T0AHmfk3U9yURT+ymQ7vxkt8RMoCN7HeFLi42P5D70HbLEjdvgkdX31yzWzD9+uo60oC8KsSWYmZmYAwHqt9THlHaLp34x7TP7a/700s+hZ8eLDK/7yb+Nn6J9XJ4/GH720dX/SWHmUUfPlJJ/3YiNl8pNwHV/NFIsKILuvyFf0q6t+M4z+/MqFSYI8B/cWkgjY3irWw/aPqivv6/zBTbkYAIp73uGq6bJrU77i44fz6VTfAI17IwcYYOB+hr73g99GLvv+kUV/9l91nkzNjvtKZ19AZVUCFPtRvRz/THnKpcv1ZWAAaCaZlNDZvvh++L8dnYECpnOKBpju5hnPMc7eOQDA9DAv6AO6zncnD9MNNg72bbvRPQ/Vk4qbwnjgWCZaELYEoudrPvvfnzon3vtFXJlJDQUGylk8s+z4pAgb4B4Ji3FTblug13+nQ6dAQIvuAFdBkifr7z3gIQWQpLCP5RMAkgC+ZyTjr9hoBv/nYebOqPh/g0jwbw/EPJTdxXbYu0FTjJ1pnI+VB8ojjFm/1ltoFoRnlczMxC4JANjcr+7mN0t5a+OvXm+wa0fZ76fqM0ocr9z2dPkzeyK/Zld9aWFZu39/GXXu9NY/MvGX5n/5IiMNJzrvL7bds7kKqJNB9nse0y/7cPee8syf+SOaLfnduZukgZ5sL/Rc9ruhYaKZfP4wHxIYqEqy0v1Ck3TP4avxJLu/u1y9mVNiRhqcw5msrH6zNzlf7WJv4j8/L5IOeY7P1DwFyqnOqezsiUPJpcjnLO/DM397Z8YFN4CMezcwWamZLqhRJkWVbuyW8VPn7ez4r+CQxYLumOLYvTzXxWfwukdDzBj4U1d/VVRnuvZQp+EBAEACkykm9mHfwuU8Hx5bJoWsH/6dZaGSbETNAzBXToJpT2ntmbRubqIKD8VuWfh7FAUmUzKC0ksvn3Q7wFAgXr4axhiASBYZAeAAXmcky+blaAL8uwc69oxE+i9GOPinGdJxvx8KFYfzSI845LtDXQ6rAOZoksVU61TLwpgJXAIGAIDoOP93NyXzTk43/mej/aGT9YZrh3+6+vCwOttXtn+R48w3zcaH4AlFeqv/NN8Zvvv0l5ExL5z96FEIs20ZQf+jI3YOABCndlS+5OplN5/b8bIQR2a2e3a8582cHoq+MxMx533oyVPN25vvyawqQTG1877+V7p83RQN6aJKp/hvfny+3Dt2jLDBmP3ifJYPmbuKntpN9m/LbFG88+0XMCQAzYzn/vroe3Kf7OmJnAMHV+/unI/7y3VVP3c8wyxFFVUkoFHV5D3VZHdhSJiTleJ+2MszHyNz6NykzTDAmIgNtq83VfBlzNkDOwZwY41LIClDRW1PzJP5zZGb4RAW8kPuoSGOAZxlCt3jpt+mkW1zy+AB2AaA1TK+XlOVVuJX8FNbw2EwgAxeyWbMwshzXFZsBwB+Z0T6v1gRFP6xbKDOSPp/i0RI+D+acbidPXkViMZ76r9LF/JYOEpG1rUi046wCMnMYswAALw4+39xzbZK+NdG88X8xn4zebbpe6/txJTl8ehqM59NZrN7/6wuPxw9xP1VA8vsr23/9gWvg5ej7JelZJG51A+V9a8uh8mBmPQjGpPfFe1XD7l1tjz/6AUKLsE5UNOzxXG8xYgv3/xuPus+n/3/VwJAV6l2fublZ3pq1Zk5/PZn/1XxcS5ZdtRmjiGhkxE3HoZvBPOa6VtdF0VNFzwNfGzAAdzMkc755WIP4AIFW+PuYQ1kkxigri58U9sfnHHxAdus7RwsHtUK0zAeKFiXpHmbw6/Pw7Jt9PPxe78zGYBq3u5qT9VJmkd1MQAGQMGUNAZwCWn325VFMVXu7OvMUJ8PpWmAiUQCCHCLQTiYoEjU4AGS8VDgB1FAgAJBfMv0+ZGg6I4d5dkAAD5nJOP/JBcOniBTZ0Tu32TGGfzsgsx9NWd2uC4c5xuV7wulG074MRXAj9HlNA0bmUa4zMw0EwAAmGNVLqyvPwnbv+30gfz33l80rFpv55998fTW0L6uL2oXZ4c6Nf4/aDg/aYbv+mbtdvb8XPOXioqE2zB6nQeW/nLPEETMW9My8qxrk+Onk39Cett72hLyYRreGObwmr3T+fzXVLVyn3QNn1PLt42gYMmjk3t3AU/mOomH14Q/ehYLZpYdAVztmGvv3PCHp7coZ26CySaQne2Qp4jBwHDaRbrmnKq53vY2czMLrK21bfn8my37lcPKgRLTxvDOsPd7rHWPmXWAnoTsucy8/p2Myx2SMz2m3OWeghr7bcjHkh5cjp1QoWzr1dCEXBLC/jYEO2hgiqaS6jucFIFzNYAiIUVSYi/ILKzIgAQACLNni5M/gpQBBGEgpkgkqxUOMxGIQAIksNCCz+9YGpAUAH5nJP3/iVVj8gWZNqOl/5lMDc/gaajc19UUClwfLU/3Wamr5+7z+tZC27YmmVmMXQIAwPldbHo++fPiu+H0r33Q2Wt3N65vRMlrs3my+3kP1+po/s+8OPhvvHz32Us8enjy8Ufn8o29H/v5qdiGJnV7P3fyPXqv89wmIGKL/hdIkcuLO2k5Z8y64tN16lkrjmyZF9WTBU2d2OXvtJfknmc8EZ3vYYDcOT3P2rD3/hI9mSn29UXJX+3fU6ssmuEUkFA1wIl1dp2rdxsIpft0bSo/9GkYYY0tGDImzg3w6cx5oaCmhxqw8br7dbjPnHEX1mywoH/42oymZ21oei9I4xbM1DxRPx11iTUXVuhOAIRpHDHUuxAmDe5eBWIVsABYAzJiXTC9soCGCMPQkNV5g0wD9GCDJdAqbCkjRnad4xwjW7YwkksnQLF7iTfe5yaksAqsQB+1IDxQCNFKIJVN4fMBILTYsgOeAGTfAE9nZ1MAAMDgAQAAAAAAwHsAAAwAAAA26Z6nGP9e/17/XP9m/1n/W/9Z/1H/Vf9S/1X/T95npPF/kktj853FisqaURX/OyPGHOGjOT1w+7hLAVtdN9jn8M2WptHWQiUzMzMAAMjupePR31f7XZrj/Oq0JOye/l6cvhoH27dRXna3HHj9t0Mf0//5fmz+/mZ4dzFss/zp8rB6Kf3aV05R0REjNl7to8YHx0iAJJ/l6ee9HCaOB7Yt/pn0y3Sqh7uq9RRwoh6IChV/VnjNm0LuBgIogClg0RG7OAmamkyie/yj/vWy7Vv3LjSQXFc86L6+iUscZ2QutesfF9yfYp26HreCjIB9+lnX9HUV0jKbU/vLWzjgD3dbsNx9FyTZkwx1rPOUxEUxJLBmgqqnfcXQzp379nZOX6xT0AwePOlFPZVPlseC6tQYkBBEsgwT/InVCWbohriRoo7U9KTpCgXc85QkLJmfvtXY+0o1rWi8KFIvJlTWrTbj0a9uiAOggEjYF1gbX1NICNHNuer6Xc+1FABeZ4TmfyeXzm6+2Mm8GVXz/5RMOIb/h7KYh6ZbFB653nH36PNhDH117p/jR/CsgDGGb0XWhpmGZAJmFgMAYBR/wzbuZMgfhL8L/ZpfCS0O/3rmjb87Ttbp1a/qYcX9ml37HfdYbN8u3d7DeJGuBbfksi3HyIjmY14/Ytvu+sQUkO8tr08R70LKRj7HJpTb+3I+UhQ3zsGdsQd5ZCD+Jdl5UucC3N6r9oYBKmr1OXvsP4MLUwrxmFc/K+U8FS9KchkGAKjxVbCpu7OKfsmlKqIh0QCjmYZVT/7oIPaifx33PGwXnv24f5uDiCIHxh3VVmcWmUD2eCKmGEpTQwQd7KKgtXpARQOaHJietVUXC/1kqVvVgO7wfts1+738jb9YKANmAMDd3IHEOwIsTLcrEERniimY8RQpxmHZZsEAwAFYVDGyx6MxTiB1KeP0mDk4WkZt0Hx6ggGxAxBaeykB/meE0/+dHKPwc0PPm5Hw/zujMfDeDBnkGwCAYlPidmx+i/8CyRyNmlWjai4zk06YCQAA6AHoOu9Pa+vXBi9u675/G42moy60+f9u9PqjJ0v9ba8XaveNdt3B6cvR3nyn7ajkTu3+XfE3R1yns552jaRl/jmbnR7lfOs93dknKgCfmzd9f4n182MjN9J7/yBNTzL5CiNuaBKii6AF9Z9EDPvJd+0qiqKShJrotiumeE8y0sC/Dc+vcO2e5RKcYsE1t39e6CeL3x1DxHXm93Aqk4LjyTAAG2Bi1Iz6wt9sD5uXqeJLe+UYQ5OV5nFO8RTzEEc0ZiAFBYKykzWh30ZuNYHj7Y5MzF7njnIei5mpFeqG9RcDjIi2SoCg8UALhNYVAtsVWSnIFEIhfYU1DIkAtZfuEsBUADICnM7Z9ssy+3OkSJgUAwLYABEACGEJlpQYenwT/8mjj58CkACeZwTu/44NZfMfKzBn5N3/n0yVsYObMtzu42cKqqB2PLR9zHbtaLRZQyUz07QYAAB3h/1Thwerl+t+/9xu5cv51iXYv9gc9kG9/4m+/Xb++91j+umvX7yvsa4/v2Xrf33p59XYPXaIdPbLYkMcD8H20pe+/O5YcEEC1W8+0X+qkfvkEF/F/nYozN+IxFJzhNyp953jmXPinzr7rbenHn2qG00NAGTCZPdE5frJYsiut+n6JG/lw66fFsXGazPQ7DxH9dOuBGXuYpST182wVCZ0k7gcpyAggAb6nuHTlR+RTw55T0EnBQkwOGlYG0gqM6NJDJDATYJbTpg1LpSqgW6HJq15yRVqcnC9CijWcq8RICkDwIABqdb1lAioK6mSEgKZOBEjWiICC8tkrYKMjAV4AMaGmjJVVFREL/dvICufmhsAVZIgrpjfkr0CQtAWrFcZtOm3RQYZAwggHQAGDHYkwpZwfwp+Z0T9/yQPBz8bMndGwv+foAbBU4xLec/u1ue3xd3W2LqRn35bUdr9bKxtaMRUKqTLTMxMAAA4drzecfL+3ovD+7v20uHDeX+mp/MnDR+n+e7fP8uw6mkOHZEX+tbna40Xf/rD3/fe79Vhp/RiBAeacKqmc3I3AhCTsWzxdXj59SIfOXCJYHs+9PvJkHy2HBEuHsDXoVx0fBxVDjHVuczGg5uicp6arOCY399ZF1Py49M3k0pJ6pUGTAPVaXie6Zzyy/XJbc2Qxe7kqW7zQjxAd+5havH0gPYeduOrO4vPpBrEgOSi/eD5sqdo/5LAo7KBOWThMHGniKEbElZeXPckCqSBbCaB1ELdZHRMGQ6qq9hGVAs4ABUJg2RF6pSmTsWGMIAZJz5V2MiGikhDgUDRUoGNXkKMWrDEGWIAwAKgVMAUGkIwCA9bvI0jkTA62BInOZNnxgGIAf5m5OX/HRPGENxmmzojkf6fkrkyh79DSeV+nvchHJ//Z1ej8DxWe7foqFmjURlMMjOBGAMAboeeFjOXl/MvXtZ9G2+bV8++rBych4ufxfS1yb/b/zzD0zxlc+ifWFz+epmXl7/eXbXdc7z/o+kwf9kcD5+WXImhekPxK/5+b24gf2rcM+HrzQ1zBzEMXA6P+6UmMRl8Se5zmaZ1gEu8ldzpvN53/fS9KdhUnxhVap+T9eauTy2TZ6w5fnDZQ2Zvhm7oeJg334Ge5c9QuK0m25t7hdPZR9/KCYVwxkbNHDKKO9PPBJg7Nv7hIAQxYvaYYfpPp6YYqkjG0Bjc3csCHSeFBYD6tKSuE+XQMUKdmVIICQVEArArNgpQVoG3moo8nipCC5QIwCsDiBaQ3EKm0Jt/FSRRjgEAQAsHb1EGP3RQxa+sZwesOSClP2mla9H+AnYBLKdDORSAwgBeZyTS/zu5Kqa/GypxRtL9v2NdwP/DRub2cN2/uwon5Lv/jClUbjxPmNAcYV/btY2oRahJkkkxZgIAQO/n8Nro0aH1bu/p7mevzXkxHZ+652b4fVwVPyrqq9HUfH77iq3rzHmJzs44cW62j67bLYtoqYlghm3in3zQ7qoIoOeJkPnD/PY4A1DL+0uHWW8XfcbF604D2dMR2sP90h/OPpez+mfPnJ/RJMCcfWdWPkpB3j3d/Z+PPp/f6SXlE+DQiTGCDj1Jv9QmVaju57pby8lJ3po7E35l0gnQYjgZ5TxDvrMphqQGyJkIqNY9xtaqm858IIFKkJLSWmr+1oo1i9UUtjFheRBAF0W5JZo6EDiWRUSmVYrsWSgmwbCh1bO2XRLYxMyAolxJ7G5MVKUsgLUgbypMqwZKjQgAANKJQ0mMghkf9oa6DMABgu5Brvb7AN1znD/rHwCaAH5npOn/zoBS+N6Mq8QZCff/1Bg1A/9vxsU8PNhh/kv0cb8fHsZ8LF/hwn/8FTpG22proSqZmZlmAgCA9hwa9tr6k3Nj/9KutsJHuDh+V+sz5Xdk9caNpy+i/vnT/35H26u/y78sn/OL8bMOH2v2zY17Z+ZAVk7OzA1pGnpvfzJaN9B2J7X3HwX7R73FAE6Lm9lADhPIXDLu/7fKqu9dZcjpqp/qpICpmguW4qZm9kVRQ/gqvG6699srZN7AVCdV9AQzneWA7zzjyOwX5sALcU3P29mQ2eCak5qBU1bV1S/nO/8Cn+9pfz81uIst7GxmBWSwEXYNUGU8ywAiKGChDcNg3Eqg4j5dSI0eh0DIYK8woaHGYgamABySZekPkZCJ4lDJEGAlDksXfiAiUHzqUW5OQAQPkpaCUhAcwRuPzkUL1WoBCgACASyA9/FVfwECAH5nxOX/JJPG5OcNPW1GUv6fGFea4BnzmJR7P5sCU/7FM7H2MWY7zLQtpSaZZGaaAACg/TfYdtdmmVz0c3pjiJh698PeWHfRb8Sa3Xtv87t/8tMtU3L6Ua9c3jG9/fjssLwwsoZFevAzufGhJwWG3g7dLx//fA+TbeB8+3zQp+f487jl3Pn1Yzv0s0RBb8+TTRI99AGB/NCrc5+FlE0+tyEJ9hRZBXyfX+0Olwz70E1QzX1lrdmnaGplwyQAJzMJgJN7HiKfYrmRw84YKnvonfX27LoMxNzUk7q6O8pkTBO8eqLthMFDQ1ebMN2JyhZ3YLTdtdNxQ7qBBITsQS16BZo07KAYkQZTeHGvliQJQCgUgZsKGRkiM1gArZ6A73l3EduRVwNDXkN52hFWYEUJismsWICshUJDXxgbnJB6CEUDQTjES0sNBUi0u1aUPF0FAAAAFABeZ4T+/05qxuZnQ2XOSPr/d+Q0jvAbcDtwUnFdPKhDx8veEY1RtTZMy0yyy0wAAMhxb/NPKSsfzcnB5WSideZFZczRnPRv19b/Dx561unjectffPPYyleezPsv7PQ/nc/v7nv7fELHDstnTACkqPPGN39OQwGbz7L87F/+pfIGXBwvw8FDfqIeHL3ZJN391reKvPqw/nD/89jrpjY/npXTuoCs2dpPPqf6ppR3jF97nus9KPpftRt/P3Mf8M9HQcJrIRc67va2Sfhvb+x7nqungAPcSSfsQpX1HupRVsFY22B4Oqea1G9XyVwPxgArApeBuGaACmcwHoGa1V5qwFDAKFKmYaCdJGRDEVpSe4yCCIwSqcZla+awA9ABBuiIFQChJZ0sHL1RLBtYyFRIgDRQEkB7AcRhABl+D2HuMDQOBPgEGAlsWMAIwHFAZ2yfP8AAvmfE0/87eVfwcyDzZuT9/5+sKe7QN6jA7dTjl3KOrhN8jH2u7eprtZqZuUwyMw0AgA+f9KqN+Y18O39bd1od/T5Odr3nn3u8sdZxcoVI2tuV5BePPjxV/id++/DtLTfDT99/8+jwN6qTPGjAMSKOw0G5tqfsezskxM5sJF91uvcTVF8GMfntYtLQWfvkz4X3T2lHVJGZ6I/Mk6drhmRmEwFAtX+P95zOqYYqlvj2t0xx3iyUb6LqMpVLInMxc1J199z3ZummGwU9wcA+rifiVOabDUCTLrGr9jmtWiEFOcXQwmkiLQWUnCDo7Biyu22g0tggDwY6inA3ZSLgoEGRHCQiAA1glwU2olkopQRjLeVEMTFhgYk9wuCSTYUWNkCCqIi6xKDsCgwlg1sBIh0YYQxQUCMARbKBhBgAUOD1tnQACoABy1TpkaYLcAnCim5kAAAAAD5nBOn/xEwDfwebOSNK/3eGZozp5wd63ivWF3Hekxix10ve+8Xhb46gazuatWFVpiSTzMwEAIDT9fJhbE3zOuO5P/KB7fiZPLzjx0Q3sJ2vuc1Gif4vqn0/6nxa/OKz5Q/t1H9Ne//VuR6Hp5A2T0Wtue6s/H/mgt79hac3p/64XELJjYbRlzmA8/N2XIMQbzfDdbcqyar504lyb96T+a2+KQ3g7J+huOcuzrDyltZvVS1n+Fd+zl7sk4tJs3Ume1dlvDZH7qnj95zlX2FPqfLHNOz7m2IPEiOmCG42bmLmm/eFFBWlABlXpczjEh15MALsZ9z0oMQkXsfILNlqE/TShtUGShnFnW0ognkvGMYZ0Cm5KTBlrSkQYFjBABiijg270QoylKOtSETiOzYTuZ4gihFaAUMQaNmIEoAPqGDu2f+hDxCslPbuvncukABPZ2dTAAQACAIAAAAAAMB7AAANAAAASjLuehH/Rf9X/0//VP9C/0j/P/H98V5nZPH/JJUx+X/zBLQZSf3/FNsV078n4G2dvV+Xw0wT/fzOCY91dbw5oHqxrcO3bVWjqpJJZgIGACDP64tzyVnvDv1w9Gs8dtv6w/FjL3n+8meUWDcZJUO65uPKTjTyn31ZfvEvz35hHnc0svt+icxNz+WTcfl59is7AbDxK4OYr2uuNAUWVcuifzov6SKWvyraM92WkNm/fJR4mOn7UAynslaRlQWdDOe5RJnJBO1DV48//j6Huu+7qYkYihEUaM2sKeu+GZ65yQMTYqwSHWdRRp6rURVKBkhytot8BnJVUpkIQAw5IGUBvcoAAQZwysiFWzaIiiCi4jYsekDTKgDE6bJ8MWkx5KohAQBKsFoYGMC2AGgsQ0AanKa1UayjFr6+IYzgQ5K4ZdHSEHpPA5fSBxwcgKQBwZQXqfPGYJTEdDPgAH5npPX/nRTM4N+zofJmBO7/KUMJc3q+G3re9ojixW2eRWRsvelA4bxn7ao4orBuFWW1MGvVKpmZmQAAgF6tp7Xf+Nnfp3/7OJOwtvHUd27zeHFNTxsf914a+cXer+1/2tq+N4z9/oZf2s7vDsuItLYw4QHzYvn71ponhr6YrHo/bd7Tod9ttj539XyizTnJ3uhTdg/BZZzLfOlhqvJtnNr65bXfnlh8WGPW5Rp7VRKmLxy360iX4RG3501Cshk369NdBnzRcFxFb/ztXLhpRFkHdQf196nE71MrgkAwQzsqZIQxAmsj8aVXYjQqVoUxSsMsbkVTgmbIBLoNC8Q0pBGGXqKkYBBm0VJo9YqaDizZFshgzawoTWG8G7vCnrR0YLkHl6MNyrs8hUMAs1CiwYzlhg4HOTAAmo4iCdGUPRpduPzTwkpFDGIdCeEAmGP3ngEw/IffAB5nJNz/O1kX5vT3hkqcEcT/k9QMI/TDUYHboZ3A8ZNDCv+xyOX+X+WAC4xRka9pRLRSJcksnoABAPga+D3arzXe3771DmZevpi518Gm+3excTZxGtxYrzl6b/Y+2v/55rXZ8cN35QdO/p7y7LiWT6Xb7nImvRXHbbkJqec1XyCBqtzTufyK66+f3SD5eTiSs879jj+X5lTZ6wVzdbqD6uyGWOd4k1459NRPdzp2921rJ4EZLDWFW3AbbiIyKhgSZoeoJhzHV3TU6Y7/VM2Mpcx+h76pprA1M6+JbiiGWaJuXPbFfvgXu3M28CMpHCQFC2AAqdjjNeP2hJYWALrhuMeYi7CXVg0DMpTr7ebGQCIkJdu2AdAYEJ6YSIARGINYQS3GaDAHcECvAaI2BZ8hydJELC8RNHHxSE0KAAVVWkC8lgMMdgwGAKAevYf6LwDeZkT6/yljKXP7fgt64ozI/b8jd4XgXYC377dzrW2crzyj4MtZStePfGW+VTRaqzWidZmZCUgAAN4w15ccn/353dMeve071lkO7rvGpbM2Xtz1XaXO9vXlSf2d49Dd8iov/7D53vPrf1i8nKTbe7Sy8d4x7Pn1k33vAqiC6UOE+bFi3ElxDrESH8V9JaLh0LKXlDHcQ2Y3efW6Z/IitfKezrrEzsUUR6dQgiNbHMTbbNm/Nk0pn7Swdwx0FRTnZ2p8FVdlJ3ncOSc+mVD9A6Yzy3uZnKHiJBq8DKgrQHRBQTpDBOhKw4wnndMVL+lGDSYDgGwXkTK2TGfUdGGsYsvGEQBVi1omgSKRQsYODbhwAUhSBG1kW1ufBbFGJiABSULW8glNaCEpBTJgSSlAQpEEiFEAAIEYAAyUMA2gZgXA4R50ljylHQIoBgh+SNMFRQAELQDeZoTx/2QIBX/PYCOzZuTM/zuiK4Z/f4mkmPeZ8ycjl01aY1f70F90njPHh38kxvCNEbRaS7N4ySQzAQkAwGh6ZurSme4WmMnptfu5/07Xr20k7h4O22i+va0Gft/fk1fX7h970rj9+/X++2X1wI9//dNlGRHFRNP9cRpSYnoDyf1/jOt1OdwTkD8QTqfPP7WhbtEzkJ/Snvpk+onZrEhwlH8iGTIyYDVVxXCJ+p+o3PSZScL/ci+S9bt1m61aatBbWSRIxvB8R3Iqe3ekqyoRyQVghqqJKQKw7hYX3jI0YAijDkWATJM4FA4ZI+07rOSlBlaErAxQAITgAhOHxkJYppABRIBdTqtDvICEWzIexLC4RzgBsgoRZNCGo13ufCan0OwFBkogCdqdLTYRV6bXfb9RlOoDoAecA2tnGJJiiAQ+Z6Tt/yRbAp73QGbOCOL/O/YIpv/vBtzX5+pn8fj05e60PMI+R9sR1tBqQzKTzAwAQB7/PXutb/Di/ffnYf1a7/zTVSN+rg9d7427+afBbuvbje5+6T+dmWF3e3kw7N7w+XfJ2lvntOo+n3BwcsljSaZmudDHBAA6Rh4fb7kfya1n/z4UZQxBOG64izlT9HCazO/knN081FWZ/kwzp7IalJDMko0LR+aZOeh17YsoohSlrM6ctkAMC0fOnJhyjy6Ox/kaXkbqTGc2WVVJxgKKFbAoS2JnBnFcTD1D1CSIJmAkyAQKaQEABAk0GAAHRAAGAwBJFomhoT/Q6Gg966YwjezEuIFYCTYBFqN0uxaLVllJViqDwkAsgzEgI0NFCEgQwkKxSh2kysCACYEN8PRe/dwT65ApkEipgIKc4x8PJIBEnq6TAAA+Z6T9/84wFfz8bTTSjIT8v2On0vj7bitwf3LCo9q8vS69u8bl6LPzOXZ0jJGZqpqqKpMEDBIAgOT093/zYD+/HF/8bzbkYV/Oj6wNj+Vxw714PXd2i2bt/j89nX+7X+w2Q+f/5d7IT8vtwdcfH7IMT7ug5vin71o52Qc6maa462d3F6uis+n6xX2Ml+fpprmmT84Up3l0+jIbfecdJmfIuHpm5teMDBgovzs+XzR2zjnD2dd5atvo7m/47Cy7qYskgUR78o2cetsp4/6UeJuoXTJMsYvAdRFL56NegHqC+hk9+ydawtmSuL0aAAALZjH+4no8mOjuEuHRNw7geQYAuSMQ7cWoi8NHwnNYBQAAAMXZucqBY9EyIdtEgMMz+oZarozARBywPNnrzULRm2HxkBEkToE1JFBIsFaSAQO+ZkTi/445yub5byMzZxTp/8QuZfL3f9uJuR/Qql3HorzOTE6z+9FGqwpCIARBAAAAAMA1x7i6tc5t/Dvi/8J2/H57fq1ud+PM3WlNQv3T/pVxfvTpf/J51Ev//O/Xybj9qct8/q+WJ1ne+hzufUbA0VAs1clABsNh37Iys719fedMgxxI9VjPe2DOJ4d5i3mgk/pWNbfJzurZUVS9oMQ/YmiYmgG+v3vvJuGZ58PV9Cevl7v89wYE3Pj7+11JXz35y71midw93neq+pTmJP+e85JV3cD5022cnNOCL8sAcAdmAWAYBwDAOeAAAIAHwAIAnmfE+/87+a0Yfv5t9fW0GYH/vyOPMvn/t63I3Ne3nx/ahXNvIR0re8fatqoAIbACAAAAAF4MDH9xed3b/yjSUv/wLReNDW8H02niw9njz2NDNZ/Gu5Z73n7lmdY/zPL2rU+8vXj87kv/KMe+ifnNzo4nG0Tvzuoo9dNn0zlN9bUe94j8Cu9Af8qhuEzNwHBfw56OqIbycIqtf5Zq5cF5J+Oe63iM2Zz0Qy79uxD9nTMjPvG9d+O1ysidb7mngG8W2ztZhucicwDIe7p2bsVV5J/0cSvRapJc+sx0lFC4E9ap2FfZAHDu78EAvNn+5Bg8AFgAgIMEAMYGggUAAN5mlP2f2Uzlyc//bRMbZ0Y4/TPbWxEi/2+k3Ka9533mcO0PHngaz6voOs1njFrbUEEAAAAAAAAAo//lkKWhXN0lDn5Ue+9fF9XxL18MPW1M/VfnF9H27f3AKG/TlqO+v9XL/60bs9PluHn5+rv7fGVcy9m3nShlUnl83Z1bk3dCUtf38/t/0sva++yu/O5elzi6s9jfy/2enPYfLrJl6YB5Js//Z9O75Dy7gbqKvHN+/j+vnFDXHa3Lqu/nTH43tc/0kMA8b1cW0PvsHk1n9VDXLff09MDQ7zOQ7reBHiqrdz5mvQpwxz+AK9g0AODwAAA="; + // function clamp(number: number, min: number, max: number): number { // if (min > max) { // let tmp = max; @@ -57,9 +62,10 @@ interface OtherDead { // return clamp((n - oldLow) / (oldHigh - oldLow) * (newHigh - newLow) + newLow, newLow, newHigh); // } -function calculateVoiceAudio(state: AmongUsState, settings: ISettings, me: Player, other: Player, gain: GainNode, pan: PannerNode): void { +function calculateVoiceAudio(state: AmongUsState, settings: ISettings, me: Player, other: Player, gain: GainNode, pan: PannerNode, reverbGain: GainNode): void { const audioContext = pan.context; pan.positionZ.setValueAtTime(-0.5, audioContext.currentTime); + reverbGain.gain.value = 0; let panPos = [ (other.x - me.x), (other.y - me.y) @@ -84,7 +90,7 @@ function calculateVoiceAudio(state: AmongUsState, settings: ISettings, me: Playe return; } // Living crewmates cannot hear ghosts - if (!me.isDead && other.isDead && !me.isImpostor) { + if (!me.isDead && other.isDead && (!me.isImpostor || !settings.haunting || state.gameState !== GameState.TASKS)) { gain.gain.value = 0; return; } @@ -105,11 +111,22 @@ function calculateVoiceAudio(state: AmongUsState, settings: ISettings, me: Playe gain.gain.value = 0; } // Living impostors hear ghosts at a faint volume - if (gain.gain.value > 0 && !me.isDead && me.isImpostor && other.isDead) { + if (gain.gain.value > 0 && !me.isDead && me.isImpostor && other.isDead && settings.haunting) { gain.gain.value = gain.gain.value * 0.015; + reverbGain.gain.value = 1; } } +function base64ToArrayBuffer(base64) { + var binaryString = window.atob(base64); + var len = binaryString.length; + var bytes = new Uint8Array(len); + for (var i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes.buffer; +} + export default function Voice() { const [settings] = useContext(SettingsContext); @@ -229,13 +246,16 @@ export default function Voice() { document.body.removeChild(audioElements.current[peer].element); audioElements.current[peer].pan.disconnect(); audioElements.current[peer].gain.disconnect(); + audioElements.current[peer].reverbGain.disconnect(); + audioElements.current[peer].reverb.disconnect(); + audioElements.current[peer].compressor.disconnect(); delete audioElements.current[peer]; } if (audioListeners[peer]) { audioListeners[peer].destroy(); } } - + socket.emit('join', lobbyCode, playerId); }; setConnect({ connect }); @@ -262,8 +282,21 @@ export default function Voice() { const context = new AudioContext(); var source = context.createMediaStreamSource(stream); let gain = context.createGain(); - let pan = context.createPanner(); - // let compressor = context.createDynamicsCompressor(); + let pan = context.createPanner(); + let reverb = context.createConvolver(); + let reverbGain = context.createGain(); + + var reverbSoundArrayBuffer = base64ToArrayBuffer(impulseResponse); + context.decodeAudioData(reverbSoundArrayBuffer, + function(buffer) { + reverb.buffer = buffer; + }, + function(e) { + alert("Error when decoding audio data" + e); + } + ); + + let compressor = context.createDynamicsCompressor(); pan.refDistance = 0.1; pan.panningModel = 'equalpower'; pan.distanceModel = 'linear'; @@ -272,8 +305,16 @@ export default function Voice() { source.connect(pan); pan.connect(gain); + gain.connect(compressor); + + gain.connect(reverbGain); + reverbGain.connect(reverb); + reverb.connect(compressor); + + //gain.connect(reverbNode); + // Source -> pan -> gain -> VAD -> destination - VAD(context, gain, context.destination, { + VAD(context, compressor, context.destination, { onVoiceStart: () => setTalking(true), onVoiceStop: () => setTalking(false), // onUpdate: console.log, @@ -295,7 +336,7 @@ export default function Voice() { // pan.pan.setValueAtTime(-1, audioContext.currentTime); // source.connect(pan); // pan.connect(audioContext.destination); - audioElements.current[peer] = { element: audio, gain, pan }; + audioElements.current[peer] = { element: audio, gain, pan, reverbGain, reverb, compressor }; // audioListeners[peer] = audioActivity(stream, (level) => { // setSocketPlayerIds(socketPlayerIds => { @@ -361,7 +402,7 @@ export default function Voice() { for (let player of otherPlayers) { const audio = audioElements.current[playerSocketIds[player.id]]; if (audio) { - calculateVoiceAudio(gameState, settingsRef.current, myPlayer!, player, audio.gain, audio.pan); + calculateVoiceAudio(gameState, settingsRef.current, myPlayer!, player, audio.gain, audio.pan, audio.reverbGain); if (connectionStuff.current.deafened) { audio.gain.gain.value = 0; } From abe614a3851753a82d4b999188e13c3a2b049a4d Mon Sep 17 00:00:00 2001 From: Ryan Shavell Date: Sun, 6 Dec 2020 23:20:23 -0500 Subject: [PATCH 03/88] Don't infer parameter type, disable reverb on creation --- src/renderer/Voice.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index a5c638ef..6645cfc7 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -117,7 +117,7 @@ function calculateVoiceAudio(state: AmongUsState, settings: ISettings, me: Playe } } -function base64ToArrayBuffer(base64) { +function base64ToArrayBuffer(base64: string) { var binaryString = window.atob(base64); var len = binaryString.length; var bytes = new Uint8Array(len); @@ -284,7 +284,8 @@ export default function Voice() { let gain = context.createGain(); let pan = context.createPanner(); let reverb = context.createConvolver(); - let reverbGain = context.createGain(); + let reverbGain = context.createGain(); + reverbGain.gain.value = 0; var reverbSoundArrayBuffer = base64ToArrayBuffer(impulseResponse); context.decodeAudioData(reverbSoundArrayBuffer, @@ -310,8 +311,6 @@ export default function Voice() { gain.connect(reverbGain); reverbGain.connect(reverb); reverb.connect(compressor); - - //gain.connect(reverbNode); // Source -> pan -> gain -> VAD -> destination VAD(context, compressor, context.destination, { From 7da1824661bacd62f10c3a79e32041e9c14ae205 Mon Sep 17 00:00:00 2001 From: Ryan Shavell Date: Tue, 8 Dec 2020 00:33:39 -0500 Subject: [PATCH 04/88] Only create nodes if needed, move reverb to own file --- src/renderer/App.tsx | 5 +++ src/renderer/Voice.tsx | 70 +++++++++++++++++++++++------------------ static/reverb.ogx | Bin 0 -> 55589 bytes 3 files changed, 44 insertions(+), 31 deletions(-) create mode 100644 static/reverb.ogx diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 3ea80167..8b2f340d 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -42,6 +42,11 @@ function App() { stereoInLobby: true, haunting: true }); + + //const buffer = fs.readFileSync('static/reverb.ogx',null); + //console.log("What is this: " + buffer); + //console.log(typeof(buffer)); + useEffect(() => { const onOpen = (_: Electron.IpcRendererEvent, isOpen: boolean) => { diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index 6645cfc7..fea9c4db 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -7,6 +7,7 @@ import Peer from 'simple-peer'; import { ipcRenderer, remote } from 'electron'; import VAD from './vad'; import { ISettings } from './Settings'; +import fs from 'fs'; interface PeerConnections { [peer: string]: Peer.Instance; @@ -47,8 +48,6 @@ interface OtherDead { [playerId: number]: boolean; // isTalking } -var impulseResponse = "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"; - // function clamp(number: number, min: number, max: number): number { // if (min > max) { // let tmp = max; @@ -65,7 +64,7 @@ var impulseResponse = "T2dnUwACAAAAAAAAAADAewAAAAAAALxEBMUBHgF2b3JiaXMAAAAAAkSsA function calculateVoiceAudio(state: AmongUsState, settings: ISettings, me: Player, other: Player, gain: GainNode, pan: PannerNode, reverbGain: GainNode): void { const audioContext = pan.context; pan.positionZ.setValueAtTime(-0.5, audioContext.currentTime); - reverbGain.gain.value = 0; + if (reverbGain != null) reverbGain.gain.value = 0; let panPos = [ (other.x - me.x), (other.y - me.y) @@ -113,18 +112,17 @@ function calculateVoiceAudio(state: AmongUsState, settings: ISettings, me: Playe // Living impostors hear ghosts at a faint volume if (gain.gain.value > 0 && !me.isDead && me.isImpostor && other.isDead && settings.haunting) { gain.gain.value = gain.gain.value * 0.015; - reverbGain.gain.value = 1; + if (reverbGain != null) reverbGain.gain.value = 1; } } -function base64ToArrayBuffer(base64: string) { - var binaryString = window.atob(base64); - var len = binaryString.length; - var bytes = new Uint8Array(len); - for (var i = 0; i < len; i++) { - bytes[i] = binaryString.charCodeAt(i); +function toArrayBuffer(buf: Buffer) { + var ab = new ArrayBuffer(buf.length); + var view = new Uint8Array(ab); + for (var i = 0; i < buf.length; ++i) { + view[i] = buf[i]; } - return bytes.buffer; + return ab; } @@ -144,6 +142,12 @@ export default function Voice() { const [deafenedState, setDeafened] = useState(false); const [connected, setConnected] = useState(false); + var reverbFile:any = null; + if (fs.existsSync("static/reverb.ogx")) + reverbFile = fs.readFileSync('static/reverb.ogx'); + else if (fs.existsSync("resources/static/reverb.ogx")) + reverbFile = fs.readFileSync('resources/static/reverb.ogx'); + useEffect(() => { if (!connectionStuff.current.stream) return; connectionStuff.current.stream.getAudioTracks()[0].enabled = !settings.pushToTalk; @@ -246,8 +250,8 @@ export default function Voice() { document.body.removeChild(audioElements.current[peer].element); audioElements.current[peer].pan.disconnect(); audioElements.current[peer].gain.disconnect(); - audioElements.current[peer].reverbGain.disconnect(); - audioElements.current[peer].reverb.disconnect(); + if (audioElements.current[peer].reverbGain != null) audioElements.current[peer].reverbGain.disconnect(); + if (audioElements.current[peer].reverb != null) audioElements.current[peer].reverb.disconnect(); audioElements.current[peer].compressor.disconnect(); delete audioElements.current[peer]; } @@ -282,22 +286,9 @@ export default function Voice() { const context = new AudioContext(); var source = context.createMediaStreamSource(stream); let gain = context.createGain(); - let pan = context.createPanner(); - let reverb = context.createConvolver(); - let reverbGain = context.createGain(); - reverbGain.gain.value = 0; - - var reverbSoundArrayBuffer = base64ToArrayBuffer(impulseResponse); - context.decodeAudioData(reverbSoundArrayBuffer, - function(buffer) { - reverb.buffer = buffer; - }, - function(e) { - alert("Error when decoding audio data" + e); - } - ); - + let pan = context.createPanner(); let compressor = context.createDynamicsCompressor(); + pan.refDistance = 0.1; pan.panningModel = 'equalpower'; pan.distanceModel = 'linear'; @@ -308,9 +299,26 @@ export default function Voice() { pan.connect(gain); gain.connect(compressor); - gain.connect(reverbGain); - reverbGain.connect(reverb); - reverb.connect(compressor); + var reverb:any = null; + var reverbGain:any = null; + if (settings.haunting) { + reverb = context.createConvolver(); + reverbGain = context.createGain(); + reverbGain.gain.value = 0; + + context.decodeAudioData(toArrayBuffer(reverbFile), + function(buffer) { + reverb.buffer = buffer; + }, + function(e) { + alert("Error when decoding audio data" + e); + } + ); + + gain.connect(reverbGain); + reverbGain.connect(reverb); + reverb.connect(compressor); + } // Source -> pan -> gain -> VAD -> destination VAD(context, compressor, context.destination, { diff --git a/static/reverb.ogx b/static/reverb.ogx new file mode 100644 index 0000000000000000000000000000000000000000..d0f61627871bc7664ff35661c4bbe8c32b8ce594 GIT binary patch literal 55589 zcmeFYbyQuy)+o9*?!~Qm@j~$yXX8$Bcemou;v08&cPLtY)-PVWSW(&Am}>qZ zKqX7X$7mg{y^w8`EQl1h7X`Rb5t1T}Z;wLEgm0-qqF7LCwSg zg!s2lCNXhM5P$+h<4~X)fK0_}001@s7}GLgC0WWcl_VE1dZs7KJmv=2Bhr&228hj~ zIR^igP;r~XNFV?ZM%37%ZCTq1J}Y8|SeJZ08ztT{D&$C&uY9Pz7xZQhr5{V}9446P zP%w26VGIGlGSngXs*j0jICUs7ED0lX8KN%Bc0pr)pC5?EIYbqtz`0wTq{P4bz9^P| zkMZM_7&`06894=xIZd;g4kQB~*9pFz?0*u}KjWZ+y^9zUi8;0y%47H8Or_~Au&(|N ziwc0ih6zX~;LFzGkJgcmPt(5s#iD}8GtDKbrmm;~3+@J59u^B8?h78iI_V*LpL}&b zh3GAY7+i&z5QqK~uYEVJ9^;>(Q-Of|XMCxfOocS0h2QxK;e$h~-~jVSmQbN4(l8|! z$(CB$*4Sh&lR5hO9+o*2LLb!*?=?I zFpL|jtg|jWTCPG1u8IpVRy=a#e;oq;=nHHh)H#-^wh$Dqc#}VDfxx;YEZETkv7Ih~zAW#uoLz{r#gB<%Zc4 zJ$&C_Eh3h)>{?I_V7VLf?4xniN7#Q?A23XACX2*_Ie}EZu}G%))TPu(6mD@?N%!5 zq8fq0G=cjpp@rH@gIOMnSq+Oh4~wNH3%zDvou+>b=C9doEV%!N9xfoGQ#-;!O2>|>kF8$(#g=-qm7a?kmBHK?kpwGXX zU8El!7XM62_K;d_3>%>_eM&Y2|I2q>TXI+RsF-SV8Bx}zK~?Il_sFnm4+SV70kA%U z$Q0qp010yu84w~lv zhZc-VH;Iqy=2ir&EB>ET`oBH=Uk?0lIRH~}5Ekqq6f2;9 z4hP`{0b!fqL@L=6Zit!AiQqd@1-!4?gn~~pk!Sy99k2!V7xy~~!6&7t@!U^xktc&} z{_QaWRGzSkTo|&*^0j1PK5hI@Rwr`&Xe$c)_PbO1IY@YM-_bcS2y2XAVLP2rF;i~Oua zf(6V8ilE_!WpQ$MJWEk0&tT(bj4ZT)gDo#Gwj(~W{iU@W3zo9=kQ_io3v_?uP2KV= zWQ>IQPZcB1ETd-bq*y>~ zVC`CKSm4$FmPzs}D)w4yo{s`B2WlzBLI>&VW2v460KRsE0IF^j{^9raC{GnXodEcx z^)x^@Oc$a}uFB9EO*jq{!Y}G8DQ*|O)T}yWq|puwu!}mA(?`c|8Uv875lwSkNjV7H(7aP{(fsWjPLFiB%dCaZx(a_c#b0#o zIS42DkPC{Fbt9RYzQY6rgTaQPtB+1ylB^rW+C-dXPYsir9Smm1CD#ap!G>bTBG&?g z)eR8a#Yi${={?rkRr4@yxyskg>6%qF(icT1KEkRR=V_8vIS298V7bg-_Q5M+vnq}q z-6Y)JYYH`17_uEq7oM5Nln?5{6bIEJI)-&F*>wUD01NQ~U^-$^1`-WIlOj|i3dkS9 zw7~X|Fx4A;)UZdrQY|KSg{c`;@Z(Ob80AleD9pWt$&AG8Ulj$xiK-*87LOG$a&#qM z{ShlY(@_iGP zigY`_cu^Lax=~x+f=YBmRxT$Hg$b!VB$~M-Jt0_bmb%zyV%U@& z2mtUnoOmes;D7)OfEO$i4+o&3W03brg26ASi@Nij6nGhOUc$}*!pE>9!Uw203P>55 zOW`2{h@n2f(A1(#+b75ic>SpX17tB2G3-EXGB600m-kyJ!}>|^)9idT^*u0*4ggR9 zUt!bN5x=u4$yG zr3(8m29{7agI+_|pc`X1KGP1^%jhrvWcp76gY;fCu;7V)n@KeL!8(ZYUN>r5-LbVw zGfLae+4_m-RHtVLYlwDr@O!a9<+7g8CTP}dn`2Mp@v0$c6@(rtz@24NVl7ZsyQ1+Tx*HlL7i-Y|QfQP;lm*M5<~Y#~U}&86`=SSu`FAbKEN z^Wl5IYx3L6Q9L|jx^T>2zyB+TrJ`2$vZB!IqdCgwFl-+&7!nbs?0KJN~r_2 zpS>>(e;;WZ)x%$fh*Lz2FkO>S)b=syU8B4h6;Hc-vrOKwqKoOcBsFPWI7*>5&ySH3 zG8|Lj1Tby-uwO5VQK7BEQ*knv$!^)O(uwF>tnIMbXB8s$J{U9IS~_@Yk0^HQ9TAUp z_+pbbH|R*Ehe3x>HmnwAU8Iqpui$ErU0j@Zf~NkB9Bwo6RBuJSmC~3 zG2bs1GiGLE{X&zq9?YSzZ%MdpUd9<-o}ojau^;N*$lkB4F4Q`pTtSHFhN`I|iqRB; zmR*Zh#3>#5i)R+jD;nO!hB~<&QPeCNV2%!V+$lpFge%k{&^YL}LwQDm!(2#2jTss{ zYHVdJHkwZdX6Zpox(pOW3Ie0_F?RGi7=)yWFSu!ynC?6Aqby(-1eV& zmB(z+^rY7mnG}8JMVWSbp20^Dg~|>B6a=KB<|3;HG!MzcKcJi9MMg4dHa6()^>k2Ua^=l_@S}=3BdXS`YDDCcsc%NAJGj&OZ8@m?3_xUsprI< zQ;K^`w?Z{~m+7lSbcSear%8Y3z}R}`)9m4Oh1?ijkyP#pXMD+B^_BO8wsia2r8aY^ z&%CR0*6pLoM1eRGdg{ch5(cS|z?r=s*-JWbKl6na4Rm5E zzS-wkY7#b3>AXV_D-d_#mxwiM>n@ZeL^IIvsAxJzlC_4H0W#+@I+YOs8I(E|45@mm z899x36{Mh$S?ZT)wDut++HeXnaPY<4ul-a@+z>2#3J$QTSxl|FIUrdw-=A-WTURMR z6?+Gt!n`K^W+c5ff^NwpI0OfsQ9IhO|9zo0J1GtbSVlGNH(eVe<~AUbh(8=H~x zPrlqa{sziKP&Shy#R7wx2+3$Isgm;B3nuJ(s9lgaQJ0_Y{hr`gK~<8PZ_pcQcI6s8 zO1U>lCtOF5ZhAw{WLX-4s2!aV;F1^@C%*6cd-Png-f#skA={8mGFVN&Xr5~|D zGO*&P^wTH0fBnofO~qToJS#O?_%2h^Vv4T9ypBd1&lJUduzxlBdo4rtP(h&P>WT~_ zAaAS*!ckf5H><*kMu4nFT1!au52kZBIfbMH)!{fQV)ocCKnTUD-0$!U=W@mSB2&Z6k_~5U2j3Ob_Y*io(+U+!#Orh%-l?^UfTGlJ+zyml!6eR z!?P!XIh6IdJIhUf%|C6Ai9tb&vc_yMVxJbXTJgTVk0Ki_+04Iff(Lz72BHbx(m$=0 zwiwLkEfiTaSz;hS77BZ)0ldv8dvH* zi{;_xjs(IRE70TtdyfJ83!sC$am1Jwi~lLqF|dkTI8uzg^cQ*_3t6R-o zPj~T=e#&{`C|gWMMt;WWz~I7J#>kZW8Em~G0-KdP3x(;(9$Y1c1CI-&U>aIw z^7fUvr`VD4VE2=r6Z1G!lk}jB9=obBmOe#Cck6Y(k)dQ@2E4 z7il9|^%N~Fu4=#dH&)wAkiO|#i+oib;&D0;EhOfYkZCfwsZ?nt`uCuZI3-C zTO}2F+RWT+30VS5yvM6%EK>B1qfN5K0edNQ2Y#*E4)9U*tpl@`m^VOaycCfTAO%Ns z5IC)zOH-j)#2CVYCW&vY=2pK4<^2$+T)`_!U!UGHESgo;Hy^`0U zS0=>YU z3%4<|OQGtg39qp6mMOb;C)ZI2xar?+AiSe>2lc#5Z{L=JFjT&PnD#$m*Y25;K)QuI zjadrG2T5;hxS7T<6WUD&Y8MrTR}{?}&Jj=bgmIE;6p%(ops-d+Mu(T%Qkx1vquPjH zs1uH|z6dVOaAkeZlB%bVqG+zv0bn6O=oUBz^D|HtLb%G7T~W%st-M-|5os!R#eRzS zhfn7t2ou}z`#n|ryoRp8{MKkJo)l#D&ZXtfl!gZ`b_$*nd|bP! z0E$AHNG&10T@^}|esE>&a&Q%srswLel^3A!^6iGZS0FBSW!kvGlPC&(xbNp6?2p6j zaHDYuAzSIwSVXBh8pb-?=Unqtnly;Tk^nCkFuI1*Q$CTbe;-El{dqJ2|O zTe9)wP1wa4W;{70y*z#X3%+|_{20k|n#h;^SDRxA!!^vgGw_A8YV_(_T~J>+4-K*~ zMzJKo`K%_Ck-JI7cr%&>1K=MfdpQyAlZ})sT29-UG=iB*(lI1Z`fB9+UAbPFu9{R^ zp;L0M@E3pYhm+Zq1ELnvXJ=?$@vpX*HIv>j`4k8ieRcJ&ovEa(d|T36{!#IBXZs}I zddf*|*U=sFRnEiZ+3aqI;@SSL0&Vch{3xXV;zgr+jmgs{T)YP z>+mKQLFOek@2J-MsKa*^VxkBZ(_O7l_pOIfc{Xz0q$E*pla~|FJyfYmeW^iDk)bK0 zO23U&uJ`3=?;}gT#AB%f64o8(+?W|5ax-v&)GFqxpqDu{%((P!($mF3wb2$KW`P66 zblt|uXB+-h0e(j-Dat#>pkr?3?yxrgMi$Wa8<+YYo|gf*Ju~B->jhrTHWHFl>oxh; zzlP$fR&VzmeF>Fu;PrpJ>qM6oFKbIk^7V6X@7nllu0$FNF7l zW+TD?PIwp^2~lZ_T^`eqpZR~tU>XwWWBL~ zUC-{0N*v(2o$Tv=o0xujEKBzdXk!p|X${Gv?B=|t84Uf<>9ZO?Bk8PgL!9%AJAw`VD5Sn~MfC5p0 z1z0a9tHK@;!E_G2QPAfwHZNIQ2RVDvz-%UY> zLoIzbWoXbTT78E8*&thsd{MZ6p7lxRw~(!J1x;W072&>&{iUpI-@P}j0(Ki)E3-3# z7oLaKbM-&A+9|w*2On>+9J zc_rCB$m`v#%0mCP>~&70AWjy8Zo_;{0++@)Q00SpB5rbnA3PbO%qcyzbLVR{ak_M}Twflz$$w619usfH< zz=-4D{1Vv6J5(n$n%IC-D9DI{O4$U1UBpX`vE_YtqkrXtaV9$KSS}wN$tS8k)shF@ z?(^%KBqcPbQQI z>;Ua|E?h>1^Jl=r&}w)_w++?&dcZtjW*jDxKpRx}i2W zEH&B1wG1A_Lf?8VY&lntUov0VH@aT>=>59Vx1CD@|GLPintRY%q3ta*^x{0fJa5=6 z_Rzdwkb66`t}dK%(^>X9Nqo!~f3oArbT>)^p?vSmfXtp9{m7A7wY7Ge!hPEc^|L7w z?%EEEq_b~M4mT=;0k0y*FD`GRt~Jre#8U(M2&`^4)OIR(RcaUe7a<(0W6e*8Sox30 zpAswfs;jxx@Oj|!&T*BP&e##orG%{LOYE56u(J`>{hHqSD++P?CwYN{i zKHc~X&v@fL8qNXGiopZy)G5V4IPlM*kJ5h~`aE7(#_OcC;6vx33(z@eGqetx3N3~f zL35#5!WpZ|lT>dwvi#xj>)!CcO7y@s<*>A-o%`_+H1{pjx2eO&N}q*^C#^7y+1~Y< zcs;X5+&7;PcXrh^PrPyZg zyLqBRLy?wir+1QhK7P}JHw-?S6X9!d0?VmiDKlkk*o?JC;g!#6$FWgPc ze>NPkK8b}~iErp1$*pQ|m0gYfnAp~no_f>8r&3Ep3pZPRkM9+DTn0b!fVEo+@7n*R ztAWmO@{YPCYLRotoL{xndxS#}U(rbXn_DcZv`WjDKIiPG-uUN`QV$jQk5@NbzqWoj z{ycz3o&aB-2K)MC))6`jyS+ZY+;oTbx2J37oGJuKJFZn(9=c1WQYealO(t~7vumrr zoIH%9e87atx69u+Uk~Lra|o%6F$43oc~&U!`8V5OEM<1o=iM|30mmDo)N`B1Mz?Jt zPVkU`kT+m^CaGU>A>ZVeheALY?S8AhhJC;APMIu-3QkF{+l}&qLhN~CkB~DwdspUa zX#gbPQz4L>G1iJxU*MV!XM5yMb7m8mj1}@q^WzovFXF3CZEySIT!yS1?*l)BHqGMc z1k}7Je8IR$ozySbhSGHCG>v)fZVoRg4(|Q^bcOhZTTPzkO`Nwk#hk4-9PTDK zy=*iJeYhGYnSVao+I2rW+xhM;W=&vp{BU@*fr9hORnW~yHEC|#x!~%{4r$hWu6EO;)dbm|L<^hRg+$-0uq4b0O3rBHkL!ES zmHfFGgpaH*^_{t@&;=*_S394I47;%yseMn#xZ_a z4D8&vukM`h*eADfy?Bbj-p!B&d{wCX(0tWSonbhg`=m*jA`1S0-}|X`5on>3cK@g^2Q=b?1eO(Co|za`Pao zb=Mxw&T@HPli!VgtM?OLP|9gHDJQIAiQ*_JO1HKM zM_=^KMIfQP_!)NSaO_V3^?S{wXDLoS34lI4fLPKaWF50_JUg;KWN$P%@5_flZ%!xY zI*feq@=hzSIKV*b`~KtLg#*E|G7h!THlW``b7!x`FKm*3N>#Lg^Rno7i_cLGvodxaOk zf#hm*DfIg559J-8bBQdRf!Gtprv$N)>tlwap3L^(}Mon{n~?Bk8dCJLeJhn3C1T^xYxm*9!`V!zLmMIF1z!0HkNN7iuPS zKdSXI)Is%CP~){*?3QI0^a`Dd$Tqa$L)4-C!VYDgZ+z{qBe&oI+!D~jHfyzw0FnHu zi&_IE$*vbb2BJWo8p{o+zt67O-2F_seN4W7aC=AFd1!dwZx;Dh7k2yoetV4H*{dd+ zVOz~|vwKT0$#bMIEG!3=qZd0r*^&@3lDg^ZE;!9GK*r3oj-85Q{AygXo9Y?-SLD%j zDkU&e(H>tT2+WOyFAG5GYH5Nr_eLVBbefFvsNhkx>4Ej#u>rhjAL7|KRD&It_lDFv z+xAchv~Id!hyV26uc^?}5yNX^^_|;2<^74dJ(o_H%>DxQ;d;tg;u^kOBKYG&lW+5y z;RKY`ff6?n-gW0>jrW#mS|TUnrBc?D&)ZUBmUGZ^1N1(U*&W~NG_4)G8d^mTOepy! zXjt2_kDaDQ!2iOtL$Nf5KP=UV?0BXn;`ho-0{&+XFLE0Bp2~2CaM-Z+nr-J>8UA2 z2(JLYaLsY{tFX8B&~r@+afiIC)m$Oq%NR7-Z?2E*Hr-`%Ru8^=V@==$=I=ybeQ4&b zXq;VnlC{;$=4G$8M%ZV!=mu7s`cm$*OP+Ieo_nd3%8_$87Wx!K28~PGv70e}a%713~f1ZThl#k*>8-@A?q`?&d$Jq)cB{W9hX;zE^YhqDkK5f}GKgq34isgJSbp-w@#`!V zkC}P@wC3BdNp&M&swIV_iyzuCr%AirdwOHBc=pg+Q0N`Vd^wi=vq5rhhe6*p@?e|= zlUCqMF1#X9MnR2d!tl5~Ij6UdKe@>LO|>>V8y}hd;a%OD5s{%t-ZZ@(zaLxY>tq9^ zj{58}2G*npY((=6$m)SH}tu;}+ z_wqG|eT2DIZ#&80okOf|g0xkf-sk4as0tfb%F(JMD03G)-hPcG#yj9E`&ST)0%6Vq z->IRHxtpg~4C&CV9&86ujKrl+lM<9^qED_-q-r>~OTZ+E$_l^lhU<1@T;T|Y3`UoasZ=ES2&8AAvv&FaM%^WppOfn|GJ)Xe4Q zL*2uJP+g@aQl6i5){SnT5sw|akZxsjTqE>&qRz+OF897~qPgw$qCdOssVGC@(#aIp zEDy&zCgOpxq5piPrs1TPV^h$Y^9r)} zW4O}q^4m?Xb6!l;m0S;N8K3|D$`d*LG)5}p(A{rmsXzTaC(`_FeyKjJoLUyVJ@imn zstxUlA^N!YV`TvRdUOp65=AnLyto`J%Uv>n2gY_XwfWfAGs2c;Glts;uw!j&dnn_| zl%BlNB>y}WjoNZ)n=h%*rJCxu`|us_`kM;}MNnGe^!oSj-w4rfR^Nzpdz^&uDn;GRN%PLe zclg~Og>?pq#+Y6~)xFB#dF6eeo8vA&X0xDsbQ*P45xby%`(If?NppOz{{EM{B9uQV zz3}ETUJi_z2p+sV(*G%V72US<&3`>mZK^Sy^%B}a?-!wyG!__4qao<_Ms6MP1}p|A zl-Wwe>PjOuFm%ktO2K;XC7P@xIQHdtnB1$d&?k+DW}jZK!#x3ml&`ljR`EhrVsu%` zctj?BRx@r~?ELi|b~|nB(~-;}O!Z~9?;sphQKuODfo$h53UPuxBH#Fbvf#oOAYD={nS^UtaT*xb}kcPNcWQq=Cgn!=ds!}ZPb)4cn=yo;`T%RS+sOm{T&6AU3Aw-15Y zB%vX`SV;Q2~4dhK}K3t*eKdJe~k1Dsy0}Sb29W=pCQsF`fi#fv+M|_Rol#4HD_~>+o@=Xa(V;ZX}ZvcR$=M%kmbn}ptj8~ z0Fj%PAWoJu`fFa7WyWt$GhKq}&%&pMp&y44TzRtEnRs?c;F08h;NQx*Qb7VMO)!Ur zuc7X=+N{xF0JR%v(lU|OCa&TI2;ViAECe0kQ;39YhGThVbBFbY%Z0xHt$U6&y^80* zw)uL;y}X6R>3$MK(3w>{bu$^eQ>k(3aIr5_uYhTV0P$;+l?&_ZeNI;GYN+4*lonn8`!O$^G1jPYi}>jS)g73!ib_ zZ(gPb*Rp&@uk(i{`1@=}#BWYgx^`^(ftKa%4y=S%?>H=v3peOZsDmUnSLs}UY>B}i z?sO|#1luMXo)*wCmAohIG)M`=+NWtRUQctzK^D6N8g+xQa1JO1$>Sw*GF&;bM^5X(#)?4oSTJ>yYH}ix~&a z`%yw@0yF~}3XOt>LzAE(u!9mms6RA~p85qiF?a|NgzZ>2qy$Y+Lj6?DWRVoOVEs?0P9gH9UNef44s@*CzAGz@X2u;nT}4 zl{J;ETl%igk}#)%;EZAzBpi1_inq!&@S)T3)cKR!Q?-qT-|D;;-+J08N6N0yE}JmnAK)@rf?x{-U0rr<_#ymQc}vuBXa#($sAf6$-RSgPbD)n9j6PHj zB33&WzN>O-m^d(27QJXd*=@J@oSYf!(ZK9)bjn`dx~e{%!+XL!aBF+n)L06Fr?qPd zMEE&qeDXsmpbK`@N4$p_*R>b~0^rz?zVhHYfKQI0B3Km)8C3UZ@B6j_z!2{0OA!TH zFd=Z`OMCw96>g^u#y-6Lw*7h#roScUiXoWU>DADoz{0zYkH==l_)8mIb3s{fHD~C& zy&o?2*<(D^#W@>@oe{IyQKNe{KGYldlJkAwaP#_o(>-OmcWveFv;U?hoo%7Zky1Kl zrC4*L&+n!8mA@Y8)$O44=(LPQGPfGh+}*y2>uT141Ai`bxRdo=$2LbuybwXzxZx0DSGk~LGmWsFe|2+8^Ky-J(CY-9RGYANko zfj|s7#alU@@mzetcJ-x5-E7<9mAjI^O8eDXFW(rARanZ(%~{m8D{OksuM@c1-1L@H z3n20+NhCzdc#wUT;$n&b&t_ZuhWJ;+OFe=GMlZrQ?y zy@4Qh^Ad?#7o(`j=&;jn5pvv-;k|1uQK!RX#YS+sL~>WHdp;=);?2yPQ^^t1aZ(E) z;LSMZGmTZUP;epsCI4N{X<3^}>@y?qK?;ClPr393=~XhqF78ZG!F3_Qw1Pik>cZW+ zyk7B>tw)R{zwRcy?)h}y#pqD>pk9^Y}vWf)mWR3jkZpwV2SCr7r z-KB{mPg%3hO3NwP1Lur{!_BrkJMwzcM91^K$%_b??eL?;Wc$IkTOv0bFAtIzG}DA~ z*|#l29?QZmXYZ%4Pgn`HGE_ze{mgKQa*dN86D~Ca51g+gf0wvQS$1;ug=@cr)U`ge8-t{YAE0qS4?Br zjnl@Z&iZ*@MAj3}n-h!Jx}OX`X>8=|pMSXcvRyWzKgSaCiwCeg%nplO^+(cE;Ct`- zh4Nu5LWsnj!E7%Ssq{>9A?FOo@m(Bi=aApCm&7h;Itn1L5@>5ubc6A=QQpXYj@{&5 zGS8PcZ`w{@3d5fS2-uafYZb{lJ$ZpAJcQXm%9)okV#=Y|@m!{>C~C{~t-0Wf8IWC2 z==v}Ck`$F^NZfCVLo`z&Qvo%giWp?5ib7UDL;Z%ABqcqeNy}ec{m^T>r3{6Q_*H0= zZ^0n;-j@$rt9eK`6HFxwAI4pNOJ~tGWDe0{!oHs#+pCdwHkbC%dGc~fYvvo(W>9K@ zv2DO&zEKD5(3u-M%mXN6Q`TGbb9fKkX3y*Fma$Npl-2R0iffJ>p+6>HGhZTF^S@u+JU+HS`sDXfgtD|V&Scv? zZqy1*Vf_6}lERpPvub;igYx*PdpgTdzT9c(VVOeTqW`$_-Ok$>5hcBzb!-c%y8VlL4Z|))yUy$XqX_j&@>why7MKFBdO6Jr?x&f-(YVR_^9J$}EoX zeUUEWLG&t|G`OOmh#?*(E%aw>D~)q4>W25Ri4(z*!%f|VUoyVM6H+KheKlXBeU5{p z&|*v_Mn6uk*E&HQ4^pNIVvRzfvQ+`%^L1AgjL~pWseXDEkD$c$F>`no&p)N|2Ldcy zUi`Fn@jFoba9CSh8IvV(T8@bu1!qE1Uo%n%3IK}ZtA&B<;B<3PuFlvT8<_DLw5(*R zE!<6Sg>kGX`DUYTLCea+Q8ScyXZ#zMfM0`t#7aZ1>usZ^-Absr<>^Oa4lZ~lY_*$P zMZFyF(5uT|cIh9W$7Rp=D=>&XnH{SMN9V~Yk)UsRrYLuBM#Hyy;z*$Fi<%6G>|yuo zSif(+e@c|wMfCrG>f>~WCpbusQMIDY!;^af|v3+D!-HO#w6CZWdafv{tk&S7%L zQFc%O_!QM?77~l)bBOCYy!ZqC>t;dM>cOghs3h(J-DGBHW`v7}d)ao#nxoqb*9{%- zEgFHSxFoId?eAarEN2Hee%Sod%a{UC@lnC7q$j7U`p9F%2uL$u&_WSSJf#NJ_CI~B zQW)YyZ}>e-x0h{;*Jxu_IFTEwG-wi7YXNph#-+B-!5)D~%LFe{Me877^*CUFU)$B9 zV2ZYlUP8n)8I)(3OSi%k2uEa;CmgN}r9N0w7#qIVzU@oxX^5dl0|l_PngkHQ=QCn+ zC2zb;NtR0rny^qrK5UYsxmPn16$Lr-b z>c-;SzDxP5H>Zu1)%@6D^Inx`Hljt|e`ILw()+6_RKm4nhiHpk#i1qb3>6rJKWAm+D7JQ}oTqgvE-FDUwq%CW!II zbc0ypqjc(Uy66bXKn5hMI6x->5kg?v9ZinM_F6WHS%%3~_mq={B>~C3eRuC=%t!cr z`t&8Y^;bsF7rOjp{Z9dO8W1O3hKAG-h)Npvb$|w?jW6`xZ(jo-Rz{E_sJE%D?BxP- zbuc!OX`gaGynMR8#g;_@!THLUuy;2xwlcZKYBoL}d-ct*wdt)#mH3w?O!VQgSl*Op zmTCRD-k%heX!Pr#EjThJHuleP2ydDg#q2pF>{b!lKR;v)CtWtSI>USkb7D~ znG)tN#h#-)&6U{ZX8A>>zivjPj4SWQHmQ-j>)C}rM$>wh!)u06(bZhfe9!F%;@A(K zs5=s`G*;T6s3Hv?mX=g4CYG%aOWNSdVR55Y)AVzjqSlE4)CpfL3Qg$8M}`bzbYI5r zKEW11%3kRKy3vr>6;hQEP$V|o@tNUvkPr?UYlx5+Yom)d44II}iS_D0wYas$diZN( z;}zQ2;2`Cw3KttxcI<9UeA5`?5`4&Hj8-h)mKvoOGVuyF$e0wF7JwbOq_VN|#(0Z6 zaKWY%RJgKJ;rx#AxbGTwE6JBNIawccB&0M95_O{05VWHMmaE5I-@d|-EWKQ|JN#vQ z*L-m7yUPCMXEa5p^ske_?`d}1VG9Dodd;?%pBscup;)YqMp4f9lDexzv}+V9dpArq zY8mWX?duQFFrYB6po6f(jfQ3=a5(3=%?1 zonA**6gw34Z$w71c+-9EH&F{{`xd{vlm0^GP&g7v&fjZ3{wHTzix1yK z((iR98*T(%sj`vH(f(ZjbSFK$4#q1ci@jRVOWP4bcKVR`ZljAGnfLwCCj|Pv{d@JG8jRFm-vp4Nu^geESlOq*w5Mh zk(5y)=(|$2fQs;1bcWbpLzIv)8-8?MtyC08(~}p zdfue%6F3DNsJys^f40J@lm3z+&`hiMD?Gd~)7a-H?5ZTc1*z{jV=^94Ws$Dm23=O` zi|qP_uL@-C2x=Nd8YOuXDuuzFtelw5FJoz>j2+bDfh>{w4F+ePGc+Asu62>z#U(Yz zWNjc@+;A=bZq4#}YX|*S9J6AO?yR=G`_p95nm_A}GDm~666P7)cvi+8AjFZ^vZcqe zaweUpJwe@CL$Yh4UlnGEQ>U?AIJJEKOw>ftFrC(7boTrhLAh179b)J3 zX#ixqH|am3NNFLL1DI<}btA9@y-y`+VImHXiE(08{v<>f1_{t`-d6!HTREdGSgtX0&bB;Sd7TwkoOFl^8x zqkU5W{p)a~^uG>A5FZamhFZQ6LL;GGu*Vx31a*V`c|x6`anNK#*|DbAwVcGi1>*hE z2J~-1l|8vVeY@Yfe7}3oNt5s07~hBTsZn0Sw|JYMATf@0nzrbJcU zGp26s#b}c$Y+j+yEQ~$~V3?mE1eWR?QjQ!aEQg{>$^ z;B4HjhZk9O!)M&^HTYgOG6EDjWKKp9f$O%OF4>Qx$i`&tUGY%8+|GAy*_O*9h(rNq zTMN_fy4B>IVzAQ?yjgAz*jau{5rF{OXKcLE#cSLQ4{5dnk4Jw z?ktZBUG$sN3bgo)H_gxGSgLKg9c9^SY3^hdE!}+ZZgr}z*yV0Z-|ibg4zx+6#jHyC z?LT?Aeiql_{mJG>i1eg>Z}ov$xtqV~K|elJ-JNV&g-wZ*57iVc0lT)i)EPiTz2gz@ zt*>B3!7(R$#rvESeULqvor#ul2FZd&9Nk*9$=NKV9+=ftp@V>_!KRulgX!DanZx&s zi0}|I(psIAYS#Hn;s6o_toFhRG8LJ{K=(mwItOekyjT+qFFCCcpT*o`Ux^NAUt7v@ zS+5BoJ}y6Xaep}p|Db4AdJmy?`t3^% zb*%GyD|~Qpo9S-!T5IEf5Or2zaYOAEp1~c8ySqE3xH}YgcP~&J25Bk9t++#rySo%C z?rv>y9f}Pw40HI;`Jd_z|aM z^d~wm^Xq)hE`GOa8Rl$eFM9O+6A-+dS*er!%Ob>Gztu<9+t7cMQMelj^7J45gP)ME zL=Xd+!X;&cLL41?07y`f#MaiYG>YH5o1282>EAw?`Me1Gta@qB+HIA}k@p>m51q?O zWpR=HHo?Y9;b0i#xmin2zcnY=q{RVA{W(kUjK%IfUCEfeRaTFGm#?Q!X zneW4aF?6fL^1NsZniY%)r1bUStSAz~%HdSM@g_zAw8W5brVVB+PCbr_ctP*=IjqVg z?7cbVp9XgUEFXo@EKs{g7zg*{l)@`{<`8N~$x0F1QIORD=vXELNeL8vB&0y#02*GR zQ;>HU9aN^Re{}rg|XD*V%B<6e6U6GFVdVfd^|2sHx zddYTDtfDDqS8MXrIljOJz5p=s5WXUE23{qg#xFU!n4CAQHR}62CYAfg^94~t?Bmdr z_*6!->bPHApN+yaexJ7bsMfjlx3bq?dh?MAD(Gap6(|zmvj~m`lo_$g5SK=w z-)wSJrMyBxJSRPrrD7CWUNn}(&t-(AO|mi@Z^CICD1e9zMyx=3ccCU)QfoVFag)I+ z8?>Oe;kulp3CSA5A1TnBxQO{!u3Hsb%YGG{mP}v9T%Few(J~|(8-M1_Fpy)sQz`oM zlK|-tBza#b_cIAS08cU12EAc`SiFBi+;nG<`4hc3wj=;S(s?RTiq|QkFqdd!+@>uA z5h-9Nxea1jC_{Jevr~snvYdAGFnu)CUMnPM=HM5TKPM#*7v?OCBwijb5}kI z*i2#;X$kQsR@!tfLc_ou!J+`?Z*L=#W8$=(Sl543M_JKcp0bQ1E1(g4@&3#ZGg;=w zGy2}-u#E-Djv+qWd*k)jc&N=?p7j7*yon%>nT;7MMtbe12G18|>2g$WtB7#mT63k@ z_X1R$u}Zm{yax?kYF?wr0qvr|3p&c0XhL)V7Og8^7yt<`+}Pb3{X-}GMFxOiOF90{ zAaKs`)vW=9iiR2`Ff&1JEP~G{m*NCFUwja+rJ9g*c=5%w*+0J3CCAcf2Ci`g(s5zAeiFO}WShraNCofuD7tEy1%rH^=qawuK zizyEfW@a1Q&*qC^ujdaO#_5t^FES|AeYwTG7TkN@q;=jTS`)uc~I7-#xN7 zs;sJoJMv6^wkPmJtMB!CwU4bR9unj+XnpXn1I1*M3O+6I6j3$OyPsgkrMsQyy< z+Y&FU7aMBuOE28iA4FF{44Am^&E30?Bgl~9urdWC2LJ~TUvi23TK)j&@MtM@t&i5S z9!Y1npD)&uhybMM01Gy(1qwWzOP25OJzlUB0Qc$!1%A=eZg$$C)$eP#Wt8WJA#`k_ zZ8}}mT49Ia_mle-Db;C&Wa!Y7;#y#%2G2HGm!2DR)n3H417(=$m4RP)l@Su{u9$@5}gdu>{5Kcxej>qhj zBmcF?Dyl^g0l-77c!4d+u5jDqQHrKO$8kjxd_+LcE5nK@uh%&(i-y4cmhRhp6%oVP zHI|=Ywft#zhG*akz8p4B2`$#^Eq`Ts4R7L4U_ZXUr2LEzMkN0Y8^VCY?gE1D{^`)n zgsg2`Ds6IxEXzh+>BMI{@R`JuJ*aKhKV5EIGI%EaCBHV_9Ursi?3HNhZu9oj2%6{c z$TfUbRC)F_-we~g&3*jXo|u&M5@0-hsRC%vCjw*w@{F=G8ynZxQ3)SNjb7x%zEyZt@ai*_C^NZyCuuFJWk z=%`$IM79k%1xBiG4CQiklQTjt-hKQUJKl3G9*RqwhK{5tEa>4jjID9V?zRks z+sm$quf>Am0VE8kAm@dAYNy;HuQaGIRVuA1eaepy=>~{MpV_@TtG`ozH9U_|)ixeZ z_==FsA(nuQfQ;zv3{VGz7k3e%GsXUPFDV&3zM7345W`2gH|!4al=qHzLr3S08SGXA zVb=DS%+Sl^)+ADWc@=}u4H$7LSD%OQkH)DgOYs5<*jk=Y!#_bXtznjIQ!AcDggv{z z`(G$V1k7;4o0_Rg=`p=rtQYM4e%cIkr!fZkgywS@iZJJG-r))O--d;{S-59!Z+>)7 z*st+(@O**h8HiSlDP_#!x~F>jq4ZtvDtkaWxj8i@buWonk$E=e(3AbbE{}XvmJv4F zI1U0|es}yj%CI|H_2HK^Mg_?oxh><_FMk}qA}pjddUq3l#1?|KAn-1^R~^oC64GgT z|JF0S+m@DHW~ST<$v+6 zWwGD?F}^ih$ROn9M}HNWSK%2E9~jfSw8Vf@eK?kz=o3Dlm)McYmbQu)%aBqKWfr`b z_=JR{NhU`xi}d9buU5{EeSw>W>-&H-lCf1d^6}bC>KkDIrh5%~OBQ0oF+zBwUAV%B zj}C_b0}%Bhe8dg)ee0A*42|3PZz~|;Tex4I66CkHi7Wb<*l5L0U}(ogB~`1S=& zbMoe(xg&%$yg1RfUusiq49n)Rs<5zG=@#e04f;1*=1Wr;Rfm7qjVI;KOlj!ua-hrX z5aYtU_ZnDIV92eIqBD&0Z1-0C`k=a(iWMhV4{PL7`w=ay?e;K7u2W;omD1ZLXR7wC zpD+7(UlXk&pfNWo5>mGSpd;3ERW;H_V(z84K0FkU&h-Pv{xC7!6O;}zLQb(K zn~VJxFd*}rt$O>tl2lW02A@hLUkfT4LPfmJZ&D@=R(cGMH{~;EymJW@N=hoGXz$cN zh(?tv311o*LVr+{t9k5eP-^{3D#Yx3BjC8) z%OUS>vvH+w4(dy-Bo!#OX-%Zy`i5Kg6HY86P>&?EtsZ))EozR_xx7s5l@Q-#E;f35 z2|bZiRlbk^Vk~iDsmD{FQAp($cqlXz(5JN29)~XMJPGW(&uXR=0Z8-kcw&oWK5~0u z>;&J8{oCAi`G?t&dWZ|w%B~b$3d-_1o(SIE?>_VSnq836d@ewtCUb5F@#{Cok?40i zIKS_k@kdeap<9&770@#G3EeFWcuYiG4D*>bFXFe%#=Kdz zmAc06@-D0?_^Z&i8nZ2pVA@k4TY`Hb=!XjzqP?bQ|%I1jG{0(SCUT z$j%?5`u@L|LVD`|F$K!&VG598f&!ickAH;}-me;ta8*llV?#rIBYhp?S7FC2waQc( zJVKpj;|i3+e%)_GsSN+jDM=`0S@n3iNCL`)_<--GOFJ)o^aMAuT&y#)Ck-}!r~n@N zFVPpZ67VlCHHGW8EiWJ|F1UtgnVEb0$JWg20g46rpmNJc7<_|t5|T3}32%6I+|0er zDctpm9cB%iydc2DCa{~X-KfxgR$DW5t1-0nGn+YpTcKgDd* z5HsJMfGO30o)UfaF zTnJ^g9h&OdRrI)7o3;j%k}Pnf9d9>Hh%V>xqgH`p^dpEkb~=1?9~00p=cPEEs+^bV@G9t*JB;qBv>CFwptY?u&*UA%ievfF0J{J4G&WJFv9vx!009l*s7SPk zqgN3OdJkIZ8u9Lk3(V!T4S0%`{R#aDX8=86thWI8SEPsw1fYdV%UOSTzyK8Uv7T|` zz%u#T;R=k|*_(uIIA5P^sDDFlJ)P5<3yW%0TI5@8Yi%<~=hi(S%mLDdKArpb{1L1= zslLhNsnyM#Tu-aNfP9`nsQ{FXi4rdB;9r{Oag} zA0RLZ3Rs)ZKKO>dgv^L#C;57BgvuP|mF9rdDPtHSx86yX(eFg$bl&)nt^5TXAmy51 z&^T5~ZO(Mv`@IL>whKZ!{{R75g%f$+snbD&D4SdOK5fnT0fK?CqmxytJ50o#%)Y(4 z=Dn%Q=|r{bcEIj2h(pkC6lVK2n2O!R#l3Wdj5s9E`rP6deoWUE5g|Sy=J$lPB#v&q zwCsd30pT@GdMZBMQbk1jzp_OfY)h35{*ITC2DNFp18kXJX|d(z=iH12-{-qy6N|PR zuh@_MWDeVV`R*?LGHCSL{#Lz_8FoBa{OX2#%E-`-I>P%MU`y6xLnEej4|_M5Y>3It zmsZ+|WrZtC`k8^&mYhBmNUehPx8Dj@b_I)+O36xq%Z}o_?EDUcyrfIEw^>{E#6N{N z+C@xiYXSRlq!it;-1R@H5J%%PIH1f;2AY;%(!jgGJsfvZ1S=cx;H@-G^MkrAN%4>K zt+QJfhLGTH$p*h8*{76G{bI9$jDN_VqSY_g7x#~F0NRW&+vwIV=+_Cr}wB~guO#k=z3Nnv;Y}rUEm}n%RJ3Gj$Fj)w6k(-*#UaGVra{^ z5BRJlIHwYcrxBM$J2|k-_7R}LjBaa2B_{eTz)RvTW;1jM!J4w^9F&MtXZ?bZm6|5s z`rifQ2nC5!`EC5J_@s#G*`g%y%8hU%uu=*1&f1^Nq zp_DSb9dy4*>1wxHvvyRFUxxy%c$HkVte~Z-4`>?2j{sq&3@NGAU6Kq`dp85L(P>palxTI&H5GsH>emgQd zYnV3K>@W|{7JIQ=UFgy6y@6@h+x|*1?S%&HhM-PDIv%D9dS@=km&%UYDQ1@6-1ulY zg2`oLL^}Od88Nicuy>-HB}eA2y%L+?y(48PxfbpH{y|BKHS@QoN2?b zW9;80EQip|fgXaS4O!TnWLEXiW@7U4t=(vCfDJUH*Idx31C-R;kPO<*xaI z9E+?2UmbXpV*z+Yyke@aLTy=q)6WG`0W5l&a0NtLn>enoR#`6iKk$a={EMg+S|}N5 ziOkpwhlr$=(atAwFg^wNj!MrVgaTu)`!|kI9^~KK0bZ0_&cc$tbOYd|zbzRLr{RD* z-qb=Gm&aY#q?gTJ@RvTCpqoFrXSY}+(Sp2(SFWz^6vW&DjSLPB2!Iuy)=^xoblyHm zv8}kS`ZV`FIa`sdZvMZj72Wyn)P1?7z3%$YP?M8=D4AcCq=d=nCnW^v%IR5S+(VJo z`R9*MjxG0|>wWDpDPfOWvr+1SNMJ^l5dJJ70O~*xCp3AMFHn-KIdj4t8U_~=)qY}( z8);$#kQvoxWq~WH9?Xd0q*|=>HO7OqmDxt05js-W48HFi?KChci=q&w7a8Y}T8&hW ze6_ZnnLcJdZ7BT&S^(A;?KtRt-)kcXp~xaM$Z+~j|&YUwM@=FsOAf*VpSVc z`?iKwE8z|$;9iOHHq07NBn=`pclahccbaaI>N#Y6?gapNS=r=y(R`jSHLFiOoD=83 zPy)G*IW;%DDNfCfK{j(>j+mKTVqx|XS}CYrO%uYptZk`&zjA@^7w0?~HnWSV8CL6w zNU{yx$~uT|wf!Qf)Z2dlyxG3fb?pe!i@Xcnv?ZG_Or;yYjs3Oh>r;GMuT|(LGqR_#qNkk|!r6TAG6iNpJNK*TWZIE5W3Y zSh|Q5#!B8Hd4#`hJ34Hd1Dp)Q7}or(Z0o-`;(+)$sy6Hq6>GZX1aI7w(r|#BcCR%a zWi%wsvJfL|QvhBBut6mNPk_mnFS^x{Gb;7BEG=DX11(=t5VFl2U_hpwKzxYJCL05R zdK5)QkT`+QY99@$=&hB1<9Bh;2IY5o%p3qE;Zy7m&?s@rHk1ZDp_QuDNyOqD0^p11 zLy$U6P1JnZl7pU7tqVUjT>Sem98#IN$^y1`U1-lwj>`vf;7Us)q|o{36T1!tU?V+Y zP;VrUGc^22?A*O1ae%6{17MiRB>X_kmkFQ?R)L7u;Jwfneo&{R1)M8-v)@Ii9a6h9 zd|Fph@XlP+P@VxJmi3=W@f*Vy#T8PtS}YJiq0}ij$I_iBLB|&H_433+yp=7t=0Y5| zfsU0(Z0CC)*Uyo^+E*@P!LE?k#vaSb%Fj8bZ}`QIEJx+$OGM`C-CS!o*!ABm#t}EN zmW-63MaJ5-AQVeCw(!U|2$jUuRs$>Ape-YSTbN&sPIvbAMzdN};;_DKb*WNcZ;1zF zqml|*vZp(7+U7dFpfaXQ2mAz2&y>YH%YH|-IAJ%Uc^5&Ho_p^)6E}IF7U_nz9;u55 zB#Xe+ST|1Db}af9Ur^Rx%@nY&Dx@4+(BVWB1Swdv%+H;yKw>os_CZwk(ku^a2 zN?gC1tU0YZ3B7u7-%WUsXZD63#DU&dh^_ktdw|jYV&XL;s{loAaC&|xQ8ADlL8N5z z1ZF}dVR(g3qHuys)u`fFgLo%>Hu;YPKkN+{C)MI>=a^+t3=s8IITdxamzwB?@9F}D zuLDytqnG$mw;pTaGxLr0V$m$Z2lH6)zlU3T0}xYoOS*@pnBJL_NZBtxmlhIrmtYw< z$e6AWlCNN=@COZXMC=3eX>Bl-&`4{oIws$t5nf_AqPKGxvU=k9M0J&ran4SpNJV#= z_6Jdl`YFnCV12^M>pOSdlgr*ch4veM#9MxAVN9q;^y=sIfyO(D{;RBlvuVViiEhb% zGbcq_T3VRH|1gNVry0M+Nh;$;*@M3kaT+SqAn}B=zRjjZ#Lc!U?3egyx>gp{{_jW4 z!&rBF#yMcJ!Eg{im!Z7eiO@|gUZ|qP$u*&ZxjH?9_@5cxl8)^KIpLrS#*-4I1vT#) zVxL#?aktc8>CT@DAlm?n=WSd7E&FWP_20b!2<)r>_L|{{j8uLt#QKLKnx`*1A5%8Y zNviZUKh$TE2;JLsHLKU_#wu@Z*P~FCdZ*5`>qf~xdLG|1TT9e$^++op{`6?-Eg(`w zoMb-J0i7AE@IQM;iCKox*_kIEU7gBqWy>|o;Q96Qt)1otP1sRwO`KO+yt!r5=vLHq zd{y!%m9O#YRx;R__Em^!YX><`pSOc;G zR+<|o}Vgjmwp1L`JG*{|{Ia_TttMfg~$iToQ zJ6I$8`7Z>;D`Mlf{ zbTkz%y&xObU*2WSZvrUl>FWA1W%o;+a$}niPsIsIhWH>t71{W^4l9rCc*FQR`@IR; zkNxtJnqQC+&`51*!(YcX0_SiQUI`QWo4N|>ZM2dX89bX09_KKVCvQ&ksJ5OtdbnZszxZ%(BA5YS%8+ zPWmr8VT-nfNoHY?8Q5Pk=m1Q>5gM3Y0Y->H`Z8{L=kdcbz_aGa+53sFr_U=rBQM7keDw0%koeo39f0s{xd0G1E_7e# zned}~I62b?e;`tsHK#yD`lt4_c%j5zz!>|L{tT`VnXi=L0CH$!c7faFYA&TMqVb&F@dIJ_eulNgnHKphuR z+S4{WzxR1t+!H6Xc1Yxdo5?27DmFovx1XcaPC%|->~4z&%4fNkDs^;_?Qn*O2LLnx z1`tQ;qbWV5(wddI?Vs`)R2BPVExbbA0yRQr*8nCnnD>d zydT~HZ-tk@6JC)+>Z=+e7#;x+-(j6Xc&JUqBKz#d^UOeue~(-VKl5 zX?!nN8?;vi-IzxwH3&7gvM~}|E@)KJlJ@Q~v&@3vJ75o%U6*Jnl2wRi_1iGW-IISZ z&(4O(tmU3ovuL1ThbXkA;jbzQAcgl znmn&E=1*1KB0J$$PCIMYQ!+ddhEq`wQ(~u*^|N-bxETF$N_A`HmE^^w6UV-+}yNq4et7FwV5G}odm*$2LYf>CGpa?i-V zc*`{@q>^fJ-+cOG`&i=$PG<-s%Fp&f!s|(q2FitJVps#RUfTaOMg=_P_XokK0bfXs z!yE5!M1LR5qEOgXOrlsNp2wM$Psgp*qYkiDsQuEi{`n=YD++7h%nF0G&IwS42B3Zw zPY}niJBP?*Leu5zc#?lmJzs>Ind17tEHy!2#K+<$TZJ_Ifl*bqexKVLxEN~OeX`w2 zDqItRze~OJ%Lv-th1(myd$Gec#QGXaZG+q4HEu_!T5c~mBEg7C?lzvFk+2?_o8D+{ zzPpZO!O*@Z{EIxMUHQ51Wp#zshMQu6)`e<5Tg#8xM}&IzkotXc=Q27w<~_^NM^Y8e z%p&*CTe?&%ds{1pwf=ibioA!MM3kus z!OyAbkDBKGDHSGjEHRY~g7V@hYSFdOgeo5KGhKi!)!-wYZY ztW2vaJjFe}hj%C00W^38gnU=MQ#~FDEhyn2i^M)D2-N*o($+{awvUJt3jpE!FkYS= za6mB{m8@Zn^oR9z`m^h0YKiAzQVIJh0PtSf(+X(%GR{QxuLJKb5FO8I06th9#>nsk z85x=$rgKWUmI07zLb?+9X#^-R(%S~EKfDhh5w>1kVVJ)}Wt6;HY=Cc)sT}TAiLP5m zMXE&$CUK^E4kfX76d0wZ&_9WHiXzX1bU49k> zt=&ph_qr;%g|ChqX4 zrhVWew0e+U7R*HeDc5x;eJRK(%lc0Kc5D`)ci1ah;`*z^^>SELs>iamw=?ju!M|6Q z=Uafg#?aCSk@f6S5@~L%!kE2>JyA0lFx-DCMMHlo#Fb_*X~B_V({s^Vouq%S?DU5W z5}@dIMeBZ|S;1uY563&@+&8|!@fu!@Z@?E~JpcuKw- z__m(DbC%Au+>jgppbhOTRD!(x-ng-}AcwbZ?~U{+Qmb)3X?u6XcmFJRzRWARy;|=( zWhv|icCq-_Qg@pimfOWk(N}}T53|bBhA|VqVPCgVm5+C+yzn@4u&q2X0?==(ttbaC zOX=B^`VU{u&Tb|aZU>v}xUi+~w6s8@<%g7@!TLD4KA~Wd!k{rM-=GzWjOalLDyb8n zW#*`>eZPSvFxZL*WHT0Wo(~M4z7Y*%?N9u3dLve7dgl-k3v3{KGyQ5>@}x0)MDdy? z@QH275qB37v3E~cy!-IO+|lvo<{BarD~W{$q%Cg1l?L3hYb(D53?Pr_p88r(pZSXcQs#pMVY4^*>41{=vV6b0aV!(ONV;sFV%7J|Lr(QCAd1x|TR>-xmBB z>rQ3@yl0iKXC0D>h2X5xjcB2O$CB<=!JN3ezPdxI{DiM5>lL&D5@=c}gy^)CeLVNv zN}xy2#hyO{yHB4+ms?db9yV|%;DauawlN&83kLr1`zCuHs8&dDd!m^>eFRFH3->of z-g9NC^24j=3JBt>CTyHcl3P=HcvNg#`>lZg@Z37L_qY7(Jo|nH?_H*F0dJq-(Cgl% zwa#SqixoT6;2ovh9y$WOH$q%T9^MrTQo13h=!{d?R!GZzDA3If#SwcmwTZo!8B7u=Vu$sKGVUv&^{Ghar{6Y_8j7+2*q) zmR&V!m=wi}^{uW&sy_IE2@m?8c$s({!{^ku zr$d>KSORvrFGIJ@UMWdmWe55Y7!? zp{vonAe)A@utN{Q5|W3GE*0PDPR2V3!a0i;f}PeXUseRtBBZ9@g&Sh?53Jk4rXUY4 z%B31!KC5we4(+4xmf0~N;N&DfRBWW+Mr5oWZZiFVxwr&CvAVo`THP`Z7JxYvgj4_r z21Nnk>mqqgL;iQWpr@d~Sz0fHU%p~Yj-0pyi-L~ks98xdUEWn5B|*U3+4TNha*Sjg z{2T!7M1tc<#MN>irWq}Qq}*r?txCoSH?0%G_gvs>XRd6NUCG<7sbT|z!W8F(sq$@V6G~n zuCxVh#5_9Skz!%b(ggUL9hA0R&rAKjL9~bZuYEMY)(Exy`<+5do_!7))_fRx&z19ABL<5(*Am0n|${ z5ja|s&m&M78R#O)vvDjI$*)aITvVhZFp6G5uHKnIrvjl=H`@=ss}HspMus$W6O;tCL-z041O*5H73*-OdaAyT-GB$o_D`)h znfgH{B3GRriBB)D)}Dv`y)+k0TsJuBg}ayFjjLmsq#FX0@wiZ_qxt02hV)&yT|Gp? zF_rC`?`co2r%gag%(TZkv^3#}$!TKpcy9Lm5SIFpTEe5rY?(zeS{#P8NzI3}dAMpi z8|Lniv&NXV>iBrlZgQDqV_T6MaBAEjW+A>H|Th> z1^yM;I|maVoUFRUZgOhuisDOR4!4EpJ|jxMLH#|Vhy*Z{TG7G4Ktc(ZpN(9(v&0Lo zZf`h52D(ZELCRRqN~!Y9p1+ateGVCnB7fWEK}2_9@W8;G;L&rsgCsGU`vbf;f$JaG z*b9;)Y5LBbmUsjrc*l`AD`Oo|tfUM~yqEHPVk{g-kB?0D;U=^0Vg8MUq`^>lZm&5b zNUGqb>G9&j#P0Sfs`mwhhE}5iOmI<)mkf|!xkFRVK;wCrcmh*8Nl@vMQ+*g0=Iq?o;XjG^+hzDpc=3;p1w7Tv z>}nmXf0-s|XlAGVUd4Ri{c8xqOo%g}YS^*pnsGp&uOKeriAPETB8VLB5R0-w8^CH6 zOQ(;E2xz1o$~fBUY?>@)`I(vPeD>)XA`Ue@7-<@JK?5t1*wP1t+rE(|KQ1nfDd0H_ z`XeB&|C6xC7wq)mEUDkUSUDr_^Tg=r2VC?IMXCsRgl~b2ZrPt(vdE=P^i@7dhtqz} zA~HtcrA3m(S6~Dh(HihjaXJG&puN5a`$tJ`Ov6{m|1y=a{^AbczBKFwZ;jBNw&ebk zlll(oS30`-B?t7md65S$wSY`bvg{DJ8xaKnM(1_;lwn*#DAN(6xUO)QYdCpwV3&|# z23c5OSbv^Wir3rOy{&|ipqw);V*Sgz$GJaanQ^-py)#m$tA-vHX5><*RvwFP!M$Ns zmhIkmgUTf957whar$$}ReppPYZQqzAn%-=gSwBrBWmEq2~myvOljZr+;i)EqgN+Dj4% z?{&2!%g0T|)sv}IDPqsAQOTG&cYC{edtpNhWX<+BU9tsNrjyKepa%v=u6#vDLfKc# zv5RdlF**F1r12Xt;0A24s>q9PaQORjp(Ze%FAjdwakRm z0&EGxBWW*yn23!j$g$`$uX!);mDTTm5Yj!?q%Jc`S3LZfs_nJ+p8{AYPd!;C|J!>& z+@a%B?+4yQNX0wvnv+JGh?{KR$prv4YCsPC3sZcf8!lo$=@|twubR{l9=y)MPw>Li z?x8};J5S1u-uxck`n$v~yVWhzIESA0`I1|qjVCkX6fiDVGDK3%E$!!uO>mtI^*GHG(LGJ$Vm z#9$9O?m!w6A6jwTGe!ozHQG;dUI2eXUDFSJY1UW#f>z%oJz(>Rlg-HPmA7?Bif{PY zA`d7c%#ovD{Fl@)zQ&pTm(-xW?$@SxHi%zsJOWzV7$>%74u z{qlR)YRHw*`+x%F#x@8M{2~y0gs~i5+JAf1<0g8q`9|u@w$>D8lA^FW^^;M6ZB#Q$ z1z?*CX}G^JkeInUOG`?k-~pA!R=b$(*}(GWW42i=nC-*5LdVHYMV^f{lR}nHO%V&J z>-`*rL+RUc4}HJ-c#wA>zSBB`O)%RW?%9}#euI9J^MF$N(As!%Aws4!s|%_5*-4$x{%e2*@k#io#H8kwlRk`tZ=b(!`n-lTDd6}(%4qDofYeW+A1O+WqaPpwJUZl2WVob75u~JRa!g`+nnmDJCPb7&E+-kL-UFwg%;zYFo;K7bKtl|6RK%}_ z=Z8*vQ#}$51!w z{9V^=+5DF;T~nNjoQed=jCjHz|8k|x`RC|XFYo7DFwK-q?r>Ls=_eAy#RNr>hK1vS zbs4dt4Y-9ZbQQX}=?ZGh-Fg>I>b~lxXV<>hIY~1;PVNNgXxHSqp4J>Sh@ei-4$C~;wT zRLp{8<+Rya$t`?{+(uKp1%=qJ)W=_DmiKhW&=eVdkg<+l`VrOpt*(gaj}4_;^`T2h z`bHcg633|@ztB%!lFSx*oyLrhjoedIb_8V7F#ygwSB@Q(cm&D;Dn8;7^nC#){!0ps zH_FXRCxSJ!p`1~l5s76fHUYxwm;j_cWvmUFU&9F}jykC?Y4T0*NVA>u3r{)N?%!x7 znD}?HdEDi@2i=Z;71wAA4HtJH!3_t!&vpn14tzGLz?#0A{j=$JV`Vd^Clm0;gHgqn zKZ*4!j`5UCo^`eEOU#zfALTh<<7=(O!}g1$k8V$XhR(E`a;@T{A`~jYZ4&t55@D+% zI~+7^13k)_HMb9kx_>mMT$bcN>Ka$Q^Q$ESU*mS|CO$cA!9-OY%7UpyhA??}mgtO5 z$LfQb?j7M9&cy}mz%{g>)Qf$szk!pFfvdN2DG%b&=bo2R(qled1XX0T_Ua=vt~2kK zwR{)W!?o7~0>T_P_o$?zPi1_ofO3(z!A2ObZQ9;hqwz|S66##zm0uThT-k6>7~fZxghk5~XXb2=co7m!@)?qA}P~N=Kas5ya#mVk(zf z)~)7yk&`kOyj2_KND`N;QC*lwi;=34#CyNbVHspiuDJ-b`p7aA&v1pL zs`S0-=^UyH4I6tKV!)F(NiUu-vrEyk@hR)5kdMn@rI9t&jyqie+4%Of`0NBt0AeaO zNgvSP?jwd~4$}+N-NLk_pyJ>hAF^CAz0keOSr}t(+HjR*F3PDEOPVjX*jDeGhU(0J zb)9u3huIrFjvnNIwsMWYy3SP2jm38Dqq#NcNpeDwm_0&oF;`#56)#OXi=LzsM8JP5 zVd`Chwc=Sxxx}5Fh^rw^nSyI_N{_nszkiY=3S_Qv@_t0EIaA}x9O}Yfh_@pZM9f_B zmm~=qX^~Q+-@X|sQqRdMYIl`lTuH`=!8gji%9b-7m&3s>i^&?mF#_05?ui#^g%oDs zbFu;C!tGy4SZxxj_ zg6~u*z;)B5V@j}j5(~h5@zGUOR65CXYg1v@cacXCtX18czWe;Ka`%t_L4)J_3nPF2ZB~luP3Ej)t^Jjw)%bN+uuHwai z*yj1*Y=Nb7PvOb$G}ye-$hGG?_M2gv=jH(+T(9w?=NuLNUB@ zdC;PSnarNaWbrDs`fj`D37pEZYF~CGEhNN!Cp%LTkHMp%NtcQjhoX)ss?WXQ)aa_L zRc#hgVcP7wV*N6&Mw_9(W-#a68E4UI-&&nYDEyUk9Oi02qE4*Zq+zXUm5N|EPPu z6<&JcY6!s=xLHImJ1Y?E!Y3^QhZc@C(V0#29j~q|N>wUOdh3(^l5uJagTJ#W1pb~7 z=m&{U(@FgKc`g(RW*tf}?Ci%}M(`m{cv8Jf-CfQW?SUbc+7+C22a7Bnw^iTM?ee%2 zA3A=#>%XoIqD!xh&Xj1z=(=C9D`(pg)9QD181~)SElcz{YPj?L5I*&Ve(1Ay7%_Zl zL3e)$+!!U2`Grr=*!a>Lr+!X^MSua3${hmUOs41SB666|HM|k@uWxqbp zKF77-VZ0R{H1;_a?1D{G-u-cFbMHgXy??r9-IQ65ln*ueD7=49T_7JRGeqmBrql>Q z=4-nO^It)5eIGZ-&}eZnHa6)6?rBan(pc{%EU>sVBk6nSsIm;Yp8&i1LC-xoJ!}v)SlM1Ls16 zT`Wf!1#my)Be4Qf=SM?*MhNwX&C>yT8@Zq3d`IYsne?d}!}If3YS7N|WGns9Ku!7l z#H`_bTyJa5#dSW$AP*_h9*`kdxcTQcdKr_dudvesl8lxHKAMxd=P7&0{kmSX_^p;6 z1XJU|Rqjb&y}nJ}G1Yd(JZW_^t_^T{LyMWLo$?Q$LX5y>A(HwTim4p)p(PL%Dn?5S z04eM3S@J$LJ#NO|V8r;r5Jh=%z{TJDHbb9y?AD)IGL*K2F62rhAzpSC~00ANH)Vv%=Kd9$Im+ad@&H zw8~uIFSC&mkLeY^bA1D*!*xJZeuo>zLsK{=mFRU+%(E(YWE~eY(3}^0V`98(O-ePP zr$;2mr|moe<0|Ox4zDR^=4xDAy-#*^*bTMeSnr@d68w$wp^YN7zSkA2dVH~a zefw@sLf%~O(p(y%O7Yp<8@!{!%q<_H@t>h(K?!jF!o{Lx=dqe=v9N!|pY zWO9+(ZJE4|hN@yrq-yIL?RA|-+MnstO`p&>u@&E_W19Sw3?Q1mC$)O~L-oEo-*CNw zej?+jn-Ncjf)3}6^>e@g0-hNiMT0cU*L;sDF@qIFVW&Y9>l(us(5x<3R@A4r$b;YI z#~PoWwA5vyUJ-=eS-;gZ&P%Ux%1h{Q{3D5X^I}e+dpA8C9NPY^uWNe;{Hs7Ix)moc zoWha)Gu#+aYO|F-L71LBdp%<0b$$P@-w_1)H)h|SAYh4F?WnbSe^}7<)Fp0iyzW4= z)IUp3B5m}sr-wI(DJWE~I;47fAzf8nF-1&FbaklO!LUPY|qISZtx5zJrc!?z4Q>km~ zI1&}9LyzT=6h+1RCXM<0Tv)c4?0P-^Znj49==g}usgKr< zk!+jH-;2NWRViCCFcOkjVUZ@s;KeURsniUBz&Cv9fRz9uVy_0*r93n@>^{}cXc+TC z2%RaV(9aYD?miDB=sk=;Qb2*S9`um+D!f0J=#Jx+Qd>%UUis6)E{_Jwd`3hHVPTJ) z1~^JANakmbt_g`bd;&5U-P{BKqt2B8KVfHhqo-MXNFHotU3yOeMB_KVwU(tJ+`e*8 zn=rlduL;~TSCA3TcYSlzomAINHWP|^Llwt5V4ic#KwOQ2|6b{_r+?P@g?cmm>GHTm zsy*p)Qo|GBt>eE7*9`NR1f9~IN@E#X(>D9Ws7MiqnF*&i$;5O^2U03 zb=R24;mcUR6b1zYWPbHlq43THGtRmbFz`mwBdp_aaM2TLFjvl!MQY-G1+td0TKQ7a z(#gni5(u0E=M7qJm3=T+!kOv$Eu!Ru#L|bc0JM0i8X3Q6`aHh@$?IJ65qcuSwG~{E zA|TPof$<|%X-N3OvGTM}7=UoVa0yN!&Ig(6LpN!1||Pvcp=3# zw1&twq|Ya>nH&5h0p;1H=qj%7F+$$obTW077mCczj-ko|{*Iq@&+Izf0sic3CR@bG z^iyjZd+X1_g9C9adCFu66(|A56%_Pe9gEQ9hBmHa9n#a=P4*Fd?w{S8LQrimo7&#h zTBv3A`EBY4i-GR#G8c72CjSMX0mwe7gFB@ped>C1^|UXxZ^kRTtR`m6O1qjw_3Tq; zIp-laJ}l3*MEq5eTbEN6WpY7(yV!A1wd}6l{pOS32*H{?^G4NWEOVwkF1ZXPgI`k2 z-YHNm>7~Y`#@1Cu6zS6Lu};>}0adG!^gm~_*?Q@&Ow>@2E>@vospH2km}D~lsQ8W| zC_GAvlrj;T=w@I14@=(uTZ$EcneSIc=@kn7_jzTs_^%@Csc9rtN5g#e?--=B?0Qbd z+#AN_r)7e0q!r8fiYwo~SSOd9!x6$J&;f}5Wej@%&lp~_rUB`SGNz<(H@F|%^_4By zz3wgGws14J4?G0!Q=q)Rd6*n9FiUVv!MO3yQd<1?E)DoupQX*+A4H>v5A$ zyh*LxGGp3#6N3#(c-qdZe{SZUv#rY=`>;EI<2(*b)4~zj&sFNj@43(>z>_ObXy%q1 z2A%Hfrs+zuJesa*KJ2Xjlr$Ah_C$1XS>(!+7QS;+ooXQ|^~_8>cQVuh%Zj#mthR)~ z#J&Y7S9o64Oqxt_NulC*wWEZuLv_n&DyQoM@neZTp>d3WZ_?4Qq^dFGt^ zzMkv;T>*pG?_Lzq^trH3;xa$6jf&B2${Fsb`~-RDgkAdejLsm7CjIpb{e6|M-%|Qy z-3+fFEa>}*UFf)hM8La?m8dCX>gpnb@m|H^j2I=y-au58f+jcdB-NG}dvg zSf8e2Y@}E!{o>ps;l<`fqUoMziJ96q_}-~Ryln3B;`CQ}$D>8l&nu2JO!lxN+y(#A z0yvY)&c1w|fU~i_-*`)imr*{;9#D=;Yf7dehy9Wc-O1QU!b&>>-BU6}B_9t$#avCr zCf6F_&Zb99B-}vztR~M6f(1-DE=awl?>}n`ON(#7W$yg;vSvzX_?d9HYTf{BI^nFI zmmy!w@|dBdE>+=I(*p9k$n$INRW%l_9sMTMXwA-NwdT3F`ge4E#2dol4*o-&c9Cs= z75yb}B0Ta0NxytEsMXzMzwPY>&b_MCsq(KoDlzyjmvtmqJAy8p3=}~BAUiW=HD|i+c(slKN%iO zugXKV%+(Ifb~>Dq^owU<{`Q=H8D?hzV1kSD*~;pE$C4(G@8)5;O9wH(17dk9*ar6DCHL|-WT{M9h-6Axx0D~b=MqIf2l+g zH6kj$`z^iq==6U56BIenSV^*UKel%44h}QQwuEnqz$Cx&rAaX1(N1#t^X?_$wBJyl ze|A8j&O7S+ZFa&3wv~oDa@preCid4d%PFta%>B!f3ED4;jkTyJ0HYI|qOJ@X!i>P# zNghd~R}bz zn73^I!SGI*{&;77yh`X^%c49Dd9?R-uK2h{!C?M6weRhGgIkEx*b5Zbwi+cBY)uCL z@vpi|BR1PjW)<^oo*h5+y}q_yDr?Kun6OQbeY^@StIX=?zdY{QeGZ3)u!(@RTbDHr zf@iIILVQokkP4JlKkqlb%VaU>{;F`6zIyl(t!JRmB}tu8D-vJ%%lZH|6qu>+H`SIL z6O_rbZ};L6b9*InsFmX(+ecVRW_mai7eXgI!&7mrKU;q>baniI?n2};MpVkCRI)#+ zug;ZG^ht2~)rIWK>L=RDg9@2RiYSMk`V&nIQrhn5uO_Z}1>NuRQ>kjYt*e*BAi2}_ z9WT&gpU+SPymxbvGY}LgW^r%;ZeMLh!rFu_EEl9t zWY{bv8QgL$i5TE#fqaKD@xiaB$C?icJ>BQQuUZHTTha_Ys=>~_aWd^0j_GxRXy3-? z93}UxU*sWXsbH#zNv;dF6zJbbrbd!pvCE>>j30A+6Z}3mqk=`Z(c6qvG2(ctvj-J0 z(;~%gsgTjIj6v-C1aa&1S9Cg@`E z+>@gko)xL4-i&YWAy}=3Hl*Ll=IQisGsuM3lcuoLj9G>fS){DR=yL@yHI7-nxd{iv z6Qyg43FxIf3~_}cu(+3=?_~*bNC+o`>p>7^FScgK(d+qz$q=w&pK9qG?Ceto3Wuqb(j5U z?I#OF!Ld?U8pGUPj{B1mwt_5$@^%_pjIH>*-`7~YuFutXtE4n`g{~5~BBib~6pj86 zE!VaSsB*0x414Ki5FNcqTJUS7i{*V1J(i(-`628V@uJmUMIe8N>(H|wMXH6a7U_Lf zs@AlsYC1&tVPx!tlz7%3Nk#E*mST;x`JZ9%15bxa1wRVF#bK%ghdr{i_ zlyJEo!9*Z3e@vbk6Sw*uHUu+D%EY))`ppY`$b}0pIs<@sqrYr9_+WbxEFTK)Re#{H zdmQ+O!p^ogO08*&T5yQwTYDTWS-&#ydiVFa)fpylGT`ZV?-^qAm{u5?4Kulu7Jn3w zZVYCsXk+9ZU-8pgXs^4NKH4FWsX}f(vuwQ@d%pO1TI`WagkaGNt^LPmkNF+8<7nib z_cO;5$pX%>yr5|95S*y5G&tSZDKhj;=^V!;ZlZ=(b8}PY=3L$V)!@LZ`zZyUbJHzK z(1W!NG|1$(hK8T(j87U^MHDR!y<~qRPh(F2aV&)N7Kt0UIEHJX5&Wpp)@@b!W#QCm z+B@|gFpT`@lk!Z^VoBw-Y*S(Ze%FULnK~P1&lkM`h?}YveX3zhxu0{DM+FO|1eN>+ z#6jhCt)<>GJ#B3&=HUjpJe6=3eoTOz3cWKN`5M0tt~rNA2*aM-`&_4pEit!Sv@G{H*WI20Zdl8^Hyq z(b=r0)VSLdFYXgdcRDqD8oUlw0(NX4hh{m;xQ#Oa6SF&3x&;K z%Yc@}h4|MVsE7j9Q2$8Ny#uf6v?Y(b$~DUdQ;mY+jIs?Z5iP%59cC+LQHFDPsOwG1 zp(iSaX-e(zVprvGZLZA z6iJ+Xq+xzKD%h=EuS169R}a(hG{(l*`iLea9q^rg#o>!s|IK~wu@00_)X+tKq6uJp zmhk2|kg4PL2%jGoEsMZ7oaDmQWOYaZjTfx>K-DW{S;9%o^?}XH z`%5Uy4L$R_ZgJOJU{yqaL`H}r9IVApL_B-n!Ac+A!C|Wolj@J*n_nG!ln(ekh`iJ( zWuSvtF433>|5g4yYBp?72Jqe_xjz1#h!>mvSDQ4>^iKNbnvbX9sxYniBaYa{W?QwR zA7;bJrlYrN0G+ zxQ$8+p27li724V~W_W+}Q5cC0SC0mBV+fPaz;m9;yr0$t3Tl{taa;TnwqKS#hZQ~h z#e;L;u3SKuVF7}Km4|vU-R-3NMH`Qa;wsq)d?BP#j+^8foMN`3#3EEYbISI{ig*n~ zOT;{R)%}y^Z>O-Zqf$<0?BEA9t`QoK^bes9oD_^3n4~m&B0Y#oCy5#r|w^npSZH=BQ* zLfzaz@jaO?gJg|&A7&q~Eq!Z+j9Pp;zFIC{pWkfP+0L-lW3ub9+zjU@fGifm*taG- z4nZ<8OVhRLZ$90l_@n6_zBki#c_KHQnK4F_k6xitOIUqg|X;S2ZoPJ()JGCyI{@25B(=4)8`xyc_8&MLqhIvphT;4nhpK+r(Ryf`39ptEc(ST62W zkAhw@jv`w4iVQJD=LH2F@SFep=u4JBG(z0OfJyvr8M6P&sD=DjLBU6L@FS{d?oUxv zQ4rDz>4;kHZ*}x_logEi^-#FcaAYDf9N}2EiS$X)CtlUsM+LYZAUTJgK1i87@j-Tw z6JRafroQ3bq)B;N=CSmi{#Wy8DH#AsyxlA}H&ML}flFtF5LWt(SXYOvWXm)-_3bkVR|R(Dz$hUBX=qApgrXLpkE6A?RtSX?Q9WaBzAyA<6a5! z!mxvuXy$hF=%wyB()});a4IO*E0!}Aa(MY1^^!Rw*+?3Ke+ChQRAG&d^@>a>-v z8qrHza7NA1zmKALRamrPUSkhe_xot-^09=L@_P#vyR|h}c=p96ZxMIp>m0X&(Vssw zw237Ij()GiP#8EqakSx6<%MJjm*EDox0pu1B~Pn%BJ7#nTh(sOPmU@A@@=*)PSAR0 zMp`e4zbkR164u^Ipt0?CCRQg#e~#~Lwd#7GqSPA)t@f~Z^$n9;H3}fc09GCKACXY~ z@m)BE07!WR+3`{Q7LJnvKP@aglJh;-{Cg&oNZwM>{>$X0yh6sdpb)S=cc(YfZ+dXB zeL1hd$Ey{BOqc^9sbf(<2SxG~sMr!unGp%SAE@zM#xeAWz!&iV&OThXhiePj{ZTa^ zT0`D_sr+;(wQ*{_;QDQ3D3##c=`N_~HUIX+w0-k|ac*|>>2#0d_9S&LL;v&@6(QDR zyU`hKQ+iy&PmiX9#thd917~V1^dg7ppcvX;MwWlT8bMNfc6eMa4(s$1$I_tifBa4 zdTjbrom;IvF@z(0e_DwNOn1ureTEc`6(y|P#$4uI5)ahw_>dIgbdd^ps~MC08t6H< zlYm6zbo{2GNEAGrkwC+yV{X6-m{Y>0$4GlW{(iP*Sf|5*m4FP#=xrw`quYdzZm zZEpScvfpi!@!IQu9KLBCLk8@6Tzyy`-d5a`DMC6NR@^tABOe|TE`*M+HF1BMa^ZEG z3nx9oY{{}d9maw>=I<6lbx+rMq$MjK4Lw9wr^PbNYu8T1eI|TD6GSd~XD5V7njJYz z33)qmyXN5LH+H*(fBb@kDr%zs0S+)Ofk1CiyRg66x##)~Nk)#;y8Y3)RAi3-6YUNc zj@io(dsnS`NmfL0f@xGl- zUiL8B;=}u!v)WL|;+WW#+j$q8RN95ISyB^crq*t52GU&2k$=sxEU=8lznh}2q_kc< zqY=clQoH)N1wZjjO!};fqlY2clW|3{lA|)bVZud!x$!VMr~4zfBA0U`)URN*P1RCH z&!4&q8!w$om3*Sa=9^zv$;=s167VAZ0|;+mN4Sr5IL?Rz1Dy@`t@PYG0$|cEkxusK z?;XZXJIHK=d{ig#N;7?*T>8xq`L+<@XU8kDj4!5N$7iC_GXcd%J2u%m1{sjtNV$az z<{W__R)KvCE`LoKPMg_O<;HF(%TA`v2w&&^Oqan z2Lk|XFJ|-cZ1#qB;_WHx+P44?E7udcdhuv&-{-2b>#|AeWWWO7wCL7s`qPy!RGa&8 z$hBW%m9bEX`5;7kT=H~u@Gh-~@R3Mid#X3%=QHy^HQ`GgyltDlf)0=1;*KBUSL5_T zZr3Romek|}0vy)jnOWr3aqzO&D;x)T9qs;KUD5B-@LBac{5W_H-|rovNV!kxZ~ zQ>uo2SIz)j3TLa`mVi-NcA*6e)F1TsQRYV}wvh|i7`FYZk8Mx;U0hpaSMYoIQ^cz< z3CNn5Ih7=3!zF2Ls>E8Rcw`hysEJMp-$`Q^&HILrZ7 z>ez@R;+%s@MYDl;(U2sNC329++MtD?8rQepuoe?NfskhbqVz{i*r82c8^CVCi$wBqa6f9Cy5 zIkvD*_2Tv1tTEvRzYZVAd`{YLq+ucwK3xQRM^PJm@J>Ipq*T+cF3rp^1(7m6_%J7=X#GQb7*&e*c>r{;`OX>N}J%YKQA8NHcV)MiWy>v!(`Fk zR}UYO9*+dSc$S!Ft+<4rg4cHAf3o9!%jO)N;L#m^6R!bZF`7)hy+!BAW`46i1PV8D z4}8NbdZ6?7ai0$+*jX)B#e*GKv7uojK7nuP zt&!-TWV^mASuKtL-Glh%y)H<>Rdq+8)jBb$r5jU%*0E#w8QjMGeWXscMUS5XZ)RKwC zUWl3QivA_fse1b{t?cI>B z56oMFLu54O_il_>03dI|84_0zJnswKJ3HjXWDI>;h$uzpwgNn<41PSLOx%4UuYZij zQKL~uZZQD{a9tG$?QmD51bS;ozkR~fLSK1KN~cJk`-X5zNEMG`2LGpqcbGzZyG`n| z)-aY?SWyCW6CtNKKfId_^H59MtJeHbR~SA2Wps|S>l1d4SU)g1-0VX50Eb>6L?4A3 zWUi4e6|0ySI1p0=#VtziokOpg~H%-LVnjt?+=_ z!_oc$RDe^-yK~`R$T&@L^T3q3FVKBh1GHjV7ywN1`59PG0|{j}vS46^p}9K}zHMBWcsPU98rUCj$Cv=qi|K@pO>LkJ9WE`r9H8?X$s_)Ex#)V{;tAC z#%hxp+xcan1b_84&Uix@o=zoBuN}iCW~LJV@?x~d8FnoX{XRl2d-3uh;$r`q0S;4hv`-yD0ohqK*-#484I()Ii_3K!`GmY zYrmwXRCA1qsm8TM2K>|4ou9ka_aRx_t5IgZl_#>|EtO*x2?;>cAQp-U+2uaJgJ0bw zw5d>= z@2Ao;17>Li656d#6Zi&9fN_I=pqOk2K)9#;CWx5b%wq^QBjlJCV4(l3AM3!cmTQV) zZH{faoRduxOan7iGq0+y9wIPI#m{98*LgZ5Y3_n*=^=_(P@7KRs-SBJbx1MtUdd0l|>N$oZzK<~=t4 zEi~XCzy=Z7!$O#+D@B>D?ru&~Me?2A|G`F$yjs8Yd*jd%>~ZiYgWQ)irPs>wD2Kvn zdR_p1Kw>8v#EYdej}lT4t8d{mld^;0^R5B_W*#6BEcwoUI*g6^jxyHeCn^5_XTVhb zSD`_r9RfCDA-OcjNR*@HKglodD3^;p(hcQzu|zr`UE4+2mR_b!O$NUyP_G{>Ykg3rUP~J91mb*q_aai-?%`BRok{+Oci8dh zgw<_kg7*j-o!P*a2E!bvQWdrx8ohU5iJY$)0d)4dw<7|ZxHyC|Tgd(P0udj^_M&A> z5=Kg+nOSL`Ve=FsW!DA#>)AuTgIG7|2Jie2V?vfV(jVdfa=v0xqr}c#jV77Xg@?28 zEP1Hb`I{1;x= z-XDzi#LaHYFa`j?9!OHI1+X9OqZPEc$_PWeGPl$1+VF(#U2yQH5G84!gX}%7aG}a1{fa3)lTTGzL3I2M5CAP zHUH=f<2`(+H_K_8@uvauEIAn#FdmJA6B`E`Ly(|37E28!bifz!6Ydl{v{tY6*u4KR z0CI$lw3zx12C6i(!?*~IB0x*#;Js&pr&-fz?AYV;^E=}o9#yD(zH;Euq{DxbcaHb1 zSl>&pIRgv1@weA2Y0JpqXeAK?a=4;R%x%!Z@h7=Ky1oCh@(e%UZG`|ouXm9COiU5s7tN;)< z8-0pE!i7D3FtDrCf%SSI)N%WPoA&M2n!909B9UCjO=QrD~1GRA#%jzhCKFfG&{_ zY}~+regO52QPC;=oAl9z;hRNG`mhtMBkmjJFM8{HkFnz%$&7x2z-^d665(yyQG-2? zOJ`*gx3R`2{gL`sh<__$B;?MccS4Y6Mn&_(-t61^uxb{_E=|6c?oa!Lz z>yv74AI~yndKsEP3_roXMq1Jr==?V8NN;;|5`P4Va=GojzF}jpUHAQI()~uznx)|P z#t2hzdsN@W?%u--o7MhQe%H*L&QQxQAt~PNJRV;(o{HiTUyGTjp8o_DS7cgOy1iVw zJk4s_t_?HRPox5>%Q_x?E?UjAI%?Kv;n`xQfxdDArGr@GYLJ5K@rcKnljTCaZND#J zUM1DWmS(0m+8T0#HbH1K%N^GnXjPZOLLKNDw20Ofs@)5df1;hIkN*S?R-0}rAS^0%*5XeOA{`~)mVK`@}qUiR9$DJZ#MO>1O#)YwZi|2pwoR! z%;aV_?-Ta^?>uZWfdbjT_wSCt{eQA!ae;$;G=yXC0c*fVnVyKwV8`uCBf)P%k4!xQ zT#=I?riq^Q0Lh=ao*R^-#2pR!V=4cvlQO(b(1EJohi9R`5Qnn1e40sbeLgHaGLI zLU$525$&^wbyo;IIV#3(_IJp|-H+W?`Hs5=HRbbn4lP+$##)Xn$cQYJ2{|838AWer zjMU0s8y%4u(4TnMBav&jFXTgzslH3d@uq`nf7{AywyL-$0GQGSrcHutCeFh|simPa zO3y!&ksaFz?e9Qa?rA;BGrElzgcp@ULN$AY3AOXcgm?rdOt{xfqNJXynyw?JC@!CO zJ(ch@M%|MtBC36Dm#w!KA|jmfMxn{rd0|b$S`NN2c+jiM)S0D>p6$#D4C1LtKuD#N z_xb13@EZn9L3!@<&jM`ATpxMNeNGMg4c@-3`Hivah3{Rm*%k?BhoZVa;@sinGLyGXeSRv(=t0o^4%JHQ~B{>vvlGH#dTY3`;{p%`D9~ z1)_b_>tN?E#r8bmR6t)qyZ)8X+7Oaa$7Q0jwC{m`evUF{kMSB%$N8Nbla3~3bt67s?{*T2tc>o92kb!97%%pTH86Qj=R?Mse&=U3n5cvT5 zDkj=UR(gyDY>1l9lj9R|(3KRx=(xS2<*h@37`Bt>pnZ8p{sMP(AV7C>%l3|>4ac>b z)$f+gH{@#MRV~R{Md7a(1l{GT4(urZQD@MvD}kM-1qW+!Aa%rFWSem9zRB5U4>#i6 zW6NPtpw*4igBlmT_RX!xiv#^NARfeuLu0ib8qt39 zaZQ;Qj`*qsMty0_RUi8TS97$KwKqVgfa|m|Ak_nhe~et&TLDh&87Y&C?YE(~9Zz$@ z(2IOxu+vX>X(t56SH*zNOMbq6jOKBF4w7l2*TmOuEq|m)W|L#Gx&zjY+24^iU>5AT zNW<5l;vB1RC9@1wA}s7-6Uo}++wZva>x_7@UV84PdGp|6g<@IR*i*nrE{Wp^Cbj+y zEAe|(!VWo*1hhM{ZB;HfVI%jfcgIU^!5>xrd64uC#2#Gv&NZp z%$Yh-eR5149qr}C0~VGmPj^M$2D)rjT6k2syjg@59}1~4R4!kCxDx`gbT}A=I9r>v zfWfrBr2o)^1yNwJ>j{QWz2$>`km%HAwrHp8 zr-7(Y(W0#b7LIHqyE{(k4=p(2*KvyXe9EG?#k5mi9ZdL?kryfnqT4MB=EP;du2WK< z{*nGjcRHRKI;wft7e)GuJipM?6hSx^KISK1MYu$QoA3Wsi7y#7fWK~yij4I~uHz{M zl_7=KXxrhJXy^o>IU$gg#8ESoW= zU5H#@FJym6Z)_;(9TuYrueWDk!Lr2$)>&g{0- ztewBLN_ukYX6V9*5EHA<{_^)R{?7C$qJ~NlgMap;ix?8vp0@FCnK>1d+_XlM@5a7n zZIDu(E1c#(`+}BjkH4~2v{v-h&1YmC4RuV+3btG5r5bJ;{nIn;vF3*mS`0aib`ScP z8M@-2Dn7;g4&x^jh-%loag}>;tfI=TOs3cG9H~IcsD994$6M-iV^KBbxq6oAFIBih zqc*6tidIoL*lVmo?RH-IqVJ(UR4B0F091L>o3eAZ|6T~H;ZO@gmh;LmNcBD%V2m7B zx*@NzDnF(b9;f}V``R=^l6qCi8#xq~e#uIgyheFqMAGEjAc@?^!sqQ0)1I#F^J?vj z;#+%gmp+n)J|d0KWsQFNh3@V+2E9j3Ha?)Hrwz`$cuo9f9KJluPkti87~z3=k$GTh zPOYRtElCvY+KIM2`gQzn$A)Ab9m{m<)SAdHnI^A-H&}0pDckD|ZK?c(froJxf1021 zg>l;Vz^tCiiJls_%ZoEmhY&5~{p#s^ty$H~H@*S@wrp35Ft#V2^UYGKl7PYK@zJO@ zl;npqBw7(2*E3d5G(AscW*sf*OnV(iM;3xE7d&!#_cd?-%hR1jlnDuSW+2(9iF29t z92HO3?D}aRj>AVoUOD>x=4IHUPlXZupx3+=pzKWevm;DIpMi43j0%$jK)S5~Z+WE* z-crZOW!S^bs-@mB~LCs7&a2-v;DiQn~u*+p7R%%>&( zpd6fU2=56}#Zl8~!SiyLAQz{{d#Zc2O_B6NMn4S z)lgwF$RAB>S3Ej${*z1)6@kOIr)Xv;=`OJ4f04zFFC5*&hu9NyP7;fwx;j1oDmCwB zmo}|lzxEL#DF0ROcK49=5}w*K97Z~g%}#zYBkM-I#*}cfiYZDul*pzC^LYhfd8VXm!#BuNJ_x}C zaBCf7lV!qF&@io;i&WUkD2auhM9HGFRZWIZz>Mha$|?&m@TyDvz_|6D4l@?x2t z*(Mf2t@3pwR>E1#akP1J+V{|e`-^J^Dku<3BWGa;R+qM&>nQmo2WAaf7L%IzTXlmV zlgi+{zET>pJXzB#P`vd;LjZ5aznZ`Q)C5<5aIMKg@(ZKw? zqDZ=`vu3#O?}=N(e{Jn-$`xFSb4ryxF+EvnHONgbJ|zR|C!4fi#HPtFViGcBa- zM5h*5Hk4I>F@~UxIe%LXBm%c*GI2KWqdLZ=)yLUp46LRN{7G*)WUE2Z zBCC7s->dM;Wtr-kCAD$?s4=H%Y+Rr**Dcgw22Ofwdy&%@Gn}bIiIiaNXV@qdlk$ZBev4*mF0!!q zZ|qyqpE>#GLPA0SFnIc7>(81I+&pvZ;joVFI2U1&-A;$I)Tm?O*T@j?*r>oXjl2tW zMIamk`~_TV9}do|f;b=l8fohMdBXbI;-s)2XFvtQ2HiQ>Yhz&lg46I4k#RZlp)|6k z^oqi>fH4Z65^NYSmL-3Rjs60{Y4ztxizr@Zrg{PBp%8IZb7FeKE1YO%B)Q+-9{rZV z(T=zNh>3a`YLnB<*ccuM3As0!zx1V?63o(rkC=+CllYrfEr(FEEhZECmS3dwDhaSv zIfR!MhxAR3=fJQ1G%d^ZCJ7lO50s=*wZ096AI{Juk}B2^l0QVx;yLU`{Qg?-W#;nx z0Y299MG6~F2DE*40j~Dyms9HtT|RdGxnHlh{#3e0VdOzn7O0KsE%j7z|BN*q-kVR< zQ=sn2PrG1_v~lFKMI!8FrPdRe{IIh00DH<3+*?|gLS(!vry3{F zjv4WMF;I68eR;L)Is%RB{goK#Smq;v9{yRnkqq~aBqg7kH!U_$a%NEVv!ymBbm#|H z!LvU=VMc_37Gj_Ix&Zw2M$(l!_whG4>thrPPXE(cLr1C%+3Nt7A{sS41u@(tS?YAP$;0u?8~`znJy;!PT6fTI4B&g8iVv5h!fQ$ews$+FckjtQ9vP! z{vjm_1^%rP(r(iO5F5QsKiAM$B>E6Te5OI+dtRYr&2-cV-9FmBI84jsXdau-{iq#G zA750+TX%Cy9N&WsN-fyMrVC2tgaPdjWw&n@K{7mGHS#fQyU;KA_gjiM% z<4*9A)XE|U7XZx?2?0l6UD&dUOo5Qh8d`#P_^j?uWCgK#SXWR*LimvX30F)((x=S* z2JWH&Ey1bI4x))l&y(C8vTU2KSB+qceU8|*rwYBkyEmR($>Cc=`~ba`%}m^V_nIq7 zR@%?ax1b#wh2+#-FU>QG3(_~j4$N3s;Wf#x^4FBzUxjU*d{G~@*!$wm7@P5v@-X6x z8Y3>MK@F03CQI;XjuL%|(u8x6LjGCE=U0T$Kz-UNS*JcAh@L&Zcs+u`vVT`er@%m8 z0Gb>v_Xm}h zt(DakG9C6u;E;p8*##-Ts1Mg8HVOwd6Rk@c2cYjp3$ zSkMVZ?0$vE#bE^i{Sldh2KxGnd^aA<+|W#Vz#xgaB7H5X4PAbcs@#aS?x0Z#n-&9? z`6+-+=*QOm%@d4|D(xPTY$P}`^n(7%-AHI2c9&k<+rWv_xxL1|mAQrHuh39a)Yo9w zayO@5eXj2#>Z9raAnMn;>UEA-jqQQATu*KE1cHl-FE~)7guBxU)t6>>-T4ukZVT2tul$&iSKm1knMuU^}Cv4CuCA*VNIBr-3Gr`aa2A% znW2T*4IBRVFjw}l8Tl2v59+kUrDY4s`UIi?CEiCnrNfjW&AteJ2rY-S!NOofjEX{- z>>u5*cnkTscU8T#Hb5tOo_;3<9V(5O#I!Y@^((*l0vU%`vSUA56dJ-|dk6WHyq|R0 zAxoU~D*~F-BpBJDpt=*9d-IF|igYw8{3mG$YTPd|Db%4=7ZeJk!2sMNbE$;V(k}Qc z=0fN_vWzC$%NepGW1)SQ^+O#?NXedQNqe7tsIeI$C?hScW7WKV#itbo2H^)UGLS&O znMLOZDV%X5LOtA>!S9!aHjc)Kd~gH^&4?4=KiCCmgH=y`GKzd5Z(CEB4_a6KA1~WD z7xbYt2JfNCSGyHjFOwb1#X>z76&ySw-1h_0Mb^S{cP6xd|6*Ut0)3ude6u!;_t~>w zd5l3YMq+vbGhN~}VuKk|z~(yL`d+KJh^ADff0AZ-Wd!{x$7d|aecks8@|{Tm`J4=0 zTs$mA^eXPx_}YeHjhQi$xm@xDDY%{77*SM-%ko8&Szt;woiC{FkO0R+2V3TYc3)+= zBTl&(^A8#4A1;3o=zeHC#R_5+knaDqeN#w@Ca$6mdM+>JsmBgm^G;-s^6s_gN=FMm z$94-cj#J-VQY(Fu!P;`EKE6b$L^VlO0F`4T3)}nmcmGfRl{@U<)Uk3bP~a)FdC%cC z#Eepg>#*$4OmHYX=GW0^VD$L7sF!;~LdDX{Sf`)SRuOKS(BR3_Qrx_~=)}98hig#` z?GgCfND~k0kp8|SO^n6qCx3e$rC$gb`*gCUA91W$_;@&Ld(EaJL#nHp66B%wQ+Sg$qO?1b3hlL=5#O-~GjL1hr26Md zv&Ms&!_*R!rry0r%*DKlouz0Hunr`ZrB-Tso+(Z$`b|l($ROs}OT7$REglyDrlJ*8z=}_{%n=ii ziK>vlA0Wpey0@!wWxcd`H^>>9R~Sf-{ys^`1#86HOH^OY*dNSexzP=NpcGeLdhe3Y zm64Hw+E>Um1KsuC`(z(m%k@}1$Z=QJP;-2g$eAw411$JXWYUkL{)Pz){-&)D(2UJmlqDt~G!r~UHV&7< z%1YQ~w zON)t%>zT=!VS0wMvwV9JNOUO787%YackIh!R|hP+swaL#+V+wo0Lu%je4-w!WwpFb zX>*l$#z@>4jX-{C*qkB7h;u{($6bV)Ul=5UJI3;vX~@E-@UrPmRGO^{v_0M)SGDrjL?{LQ6KXok=fH|sC0^lULI#s0yR8SqgC~pkZZCfCEZ<$( zN?g_|%6UvAYmiA@qJ=$7mia(Gr)WqwKEu1FFu@9jTAmQ65yB@mw8uj1e=Qa}y|?dS z12}~6Aju#`7N||r*Pd}2F3^6?)%9^RVej%xa&xo9u$?-2Yf!<-r!EBrqb&ZF=wT{< ztGbo?X)lH5Fw9TKaxa6hGKG)xXue|W1$b3{W5bhuK@paPPfN=pWDNYug}?&+pDcYf z!ceeK2>koR&`Jp}W~kg4QvO+9!IFkPJ241X2}4e__w8FEES0(z!xTw#6d4@&-%=KV z)O_E+>!@Il5+acP+rnS|(R)gG>Ux<=mM9F2tUig?sei3LqFytCx~2xbJTA02xls(f zt{uAq-(4m9(LdRj9`kcZ62_ppgRQ_3%ixQJVWZqpIN$N5OwI)C6a){JA0QraR0qD7 zT=a{8H5JgZCTbpzEfGJ?vCSCxdb$)s3m6&N7TH!F=Ne;L@2e1=$cu%eqSrFHsngAn z^2NVe(#7{hDgJSmpxh&=7x$L+^k5kurhgM}jd=>yAOWWyKJTWmS%2 z`P3`MLmk5|<0Y#A@#4%G52GP7l74_2JuU3F0|eiZpD`?R*5ZAqcIOM-BI~>yHamqU z?{qRKl|-ncB?QZ(XjWUD1rRvIXnz)Iz05ogZe5EVQn&55L&M_KpRS8eabIP zjCqSmgXe18iQDNV>*xxV=|t(3z0*jEK%;*?%oh&ZC?9q?=jo(KN$1JTR+{&h3i9B% zkcyK-m-@_%Ce$581VBijm!6Qm^# z{wG%ssPX zyKSJ-*dPxEI*M>NWg_69L6ICJA^;6wmjyPWStz9hV=d)GF)?9*i9N#Ll1+scHb8=? zh`Pj}>U>)K;mey}oEob;we$HO(lZ#2SbtZdPWxO^=?y2MVoaJ9(PpR_9 zH0=AEVh<7{rU-5}Zer$}_k|P{r2rXMrOe|J9eFSU>}TBcpu{9z$H*+%Y7_L*hSdg2 z8O5D#D1vz}7{(dUDY+>yWvwX?U|7Kb7^031yU_|Vl%uUkMB+O(6S1G5LS<1hg4R{r?o(71O^b;BB3~Uzfv76aD>x*;AS?>=0aP(_DcH8>H@nbJ}{_W@TXw9rC z>%JMDxyb_)VyqG!XHiC;t5Fq721s(Yi+l!hRZ^J0fKW*OA zq2Fz`K6mweqRlzU-sn?y55Z-e%Y5l!s`71|+WO_S-=0`}SYy`Z8hcd|rY{_~>qwX3 z5x3_}Lb4e*>(gBZe9>@RMQ{A=T)1gOSqF6o_4ld7?^7#+Krll)DrAH)0&64jrB`A! zDprLtO2aH}MN@)i$OO6#3&b>}5r$r(q+k$c(Nbvjikc2A*%ruP&8#R*VZ0l=!k)Br zV7UjEqYcWtJcUC5vrwYJjOB(72RSeRnvNm~(Ud(JUAX+y!b(L9VjT(L0GMO+R;oKq4e1+qMDF7!aPANPrO-gIGZ=#%M4Ha6qpCqBaGeA<5E2EH4o> zaq&n^3IGLFT0p#(0}OTy1^}QQy@&cQ0N!Rq`u{0oDRcXN3wq*=$o;<~cZJ|xfVc0r zbFFQhuRNpR%UMd-k9=jmRWw?wHKKKynF&Y$0NyaymmHt{`Q6hVz3n^JSq|>I#-+11 zUflIvrOx*Co-@aXe%Br^dC0KjN^X6n+J?iQHI>9za0|4!GhZ6xj<4%=wHy{!g*-`|s0*X|#C(zyNm z_5S;1^}vsR{nKR`5yeE){c##nVtNC~{eO(>bvfJEMLke||?`egcv z*{n!#lz$=_GBRK_RmE_L`k$!W^qEP*|8g&q^}BVOty*f(TUjJX#_)Vcaw>awq^l|s z$pv7Bs);EAu09?K`aGr3O}ntPD^9gx~@bhsHu!rjP+d zfL%_jL%e{5TQVNRu((G!0kR4aWYC7AUG97)r_k&L21+0a+MO*n5m%I0w$ zoC9lT7?NU$1U_e^?f)cO0-pCkX3hxW|2y^wrvL8-+}G#oXYu&-|`4_)t#yZNWVZtL5P?tbY%ota^GdwDSI z-thT%$=X}zwCX;eaByi#b#DaGK2T zG2L)0XQCSWAU1PW;Z00ap%7yt)E00sj9NfwErp+C^* zXg%wuFpccQxPe#+HUSo+bh{RdR#{0_Ducp6EQ2v1BSS?9ND#t8u}U4J%D{jLferBV zUj5t?>yRmsNU4B=oZ}x4B!EPou9E-&K4+x;|IRQK{QPY+(u{=szhkG;`1@`vaDQ^b zquRW^F1@?P<><5XId*i6k(sKRsw$HN3=#l<{kIm6l4d1~9} z?r-?)EW4?TB!NVG@{OE&+bQGzlz5w{T{29ui@-Z89n#a@+3U?h(VJoMkP#$Kfi)yhNU&Ct0Rz5fMB@LBIm+hwZzD5j6zTuOE@krf zf7^+Oir`=(MGEZLLdY|00000z%|FXy0y-2{5#_R!glo@_E!t|NQUQ$+-Qg%Y6Q?mM2->`W)_kM!?ZfEY-d)$}K? zm%IG;1_a#r`}GY!`jqA*|DN-bRo%e((`}rb(}FL{0B{Fp0bm#h z0Khrm004jozybiCXT!Eq9V>gs z+P10yA+Q1f000193=Ds9dEMLpDAH2@@GTdO4Q~h2Y2w4#ZEYLZ;~%yg7cxD(c?*jL#AaPIHH0KD1$71OUbc zf&~Bo-e#2jpV>_1$z>%00000qyOcQrJ-EiB@TX6d+%Qt)$uQ13_Wd3{a5D~(e~~Az$kCimZQGE)yw~_ z8?)2pxOw^dyZd})T+7+FohW5ed3@cSTa$M}QrGwM`+w5Q+WYLT{O(?tIJ&dg-^=}; zoc13svSsOjc_z>QXVbgnJiCFqioA1v{y(prP}d!;%c}3^Onx`D&-9SMJa1PP(EIEj oO=tB`*Dd$-^uW;XGm!4LfgUQWcRpsl3UJ3ifGgNE0O0Td06Qm;-v9sr literal 0 HcmV?d00001 From efeb6d1ea2b9ad3a8af786162d905d2a0ac26f81 Mon Sep 17 00:00:00 2001 From: Ryan Shavell Date: Tue, 8 Dec 2020 00:34:52 -0500 Subject: [PATCH 05/88] Remove commented out test code --- src/renderer/App.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 8b2f340d..3ea80167 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -42,11 +42,6 @@ function App() { stereoInLobby: true, haunting: true }); - - //const buffer = fs.readFileSync('static/reverb.ogx',null); - //console.log("What is this: " + buffer); - //console.log(typeof(buffer)); - useEffect(() => { const onOpen = (_: Electron.IpcRendererEvent, isOpen: boolean) => { From 9094fd2f7c701ddac6b5eaa932d2e2f3d7d6d632 Mon Sep 17 00:00:00 2001 From: Ryan Shavell Date: Thu, 10 Dec 2020 00:15:16 -0500 Subject: [PATCH 06/88] Working overlay --- package.json | 2 + src/main/index.ts | 46 ++++++++-- src/renderer/App.tsx | 18 +++- src/renderer/Overlay.tsx | 160 +++++++++++++++++++++++++++++++++++ src/renderer/ViewManager.tsx | 52 ++++++++++++ src/renderer/Voice.tsx | 30 ++++++- src/renderer/css/index.css | 14 +++ src/renderer/index.ts | 5 +- yarn.lock | 114 ++++++++++++++++++++++++- 9 files changed, 420 insertions(+), 21 deletions(-) create mode 100644 src/renderer/Overlay.tsx create mode 100644 src/renderer/ViewManager.tsx diff --git a/package.json b/package.json index 264770d5..dbba33c7 100644 --- a/package.json +++ b/package.json @@ -32,11 +32,13 @@ "cross-spawn": "^7.0.3", "electron-store": "^6.0.1", "electron-updater": "^4.3.5", + "electron-overlay-window": "^1.0.4", "iohook": "git://github.com/ykhwong/iohook", "jsondiffpatch": "^0.4.1", "memoryjs": "https://github.com/Rob--/memoryjs", "react": "^17.0.1", "react-dom": "^17.0.1", + "react-router-dom": "^5.2.0", "react-spinners-kit": "^1.9.1", "react-tooltip-lite": "^1.12.0", "registry-js": "^1.12.0", diff --git a/src/main/index.ts b/src/main/index.ts index 836d14e0..8944917a 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -5,11 +5,16 @@ import { app, BrowserWindow } from 'electron'; import * as path from 'path' import { format as formatUrl } from 'url' import './hook'; +import { overlayWindow } from 'electron-overlay-window'; + const isDevelopment = process.env.NODE_ENV !== 'production' // global reference to mainWindow (necessary to prevent window from being garbage collected) -let mainWindow: BrowserWindow | null; +//let mainWindow: BrowserWindow | null; +//let overlay: BrowserWindow | null; +global.mainWindow = null; +global.overlay = null; app.commandLine.appendSwitch('disable-pinch'); @@ -21,9 +26,9 @@ if (!gotTheLock) { autoUpdater.checkForUpdatesAndNotify(); app.on('second-instance', (event, commandLine, workingDirectory) => { // Someone tried to run a second instance, we should focus our window. - if (mainWindow) { - if (mainWindow.isMinimized()) mainWindow.restore() - mainWindow.focus() + if (global.mainWindow) { + if (global.mainWindow.isMinimized()) global.mainWindow.restore() + global.mainWindow.focus() } }) @@ -49,14 +54,15 @@ if (!gotTheLock) { } if (isDevelopment) { - window.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}?version=${autoUpdater.currentVersion.version}`) + window.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}?version=${autoUpdater.currentVersion.version}&view=app`) } else { window.loadURL(formatUrl({ pathname: path.join(__dirname, 'index.html'), protocol: 'file', query: { - version: autoUpdater.currentVersion.version + version: autoUpdater.currentVersion.version, + view: "app" }, slashes: true })) @@ -75,6 +81,27 @@ if (!gotTheLock) { return window } + + function createOverlay() { + const overlay = new BrowserWindow({ + width: 400, + height: 300, + webPreferences: { + nodeIntegration: true, + enableRemoteModule: true, + webSecurity: false + }, + ...overlayWindow.WINDOW_OPTS + }); + + overlay.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}?version=${autoUpdater.currentVersion.version}&view=overlay`) + //overlay.webContents.openDevTools() + overlay.setIgnoreMouseEvents(true); + overlayWindow.attachTo(overlay, 'Among Us') + //overlayWindow.attachTo(overlay, 'Untitled - Notepad') + + return overlay; + } // quit application when all windows are closed app.on('window-all-closed', () => { @@ -86,14 +113,15 @@ if (!gotTheLock) { app.on('activate', () => { // on macOS it is common to re-create a window even after all windows have been closed - if (mainWindow === null) { - mainWindow = createMainWindow() + if (global.mainWindow === null) { + global.mainWindow = createMainWindow() } }) // create main BrowserWindow when electron is ready app.on('ready', () => { - mainWindow = createMainWindow(); + global.mainWindow = createMainWindow(); + global.overlay = createOverlay(); }); } \ No newline at end of file diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index ace43b85..519aa46b 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1,5 +1,5 @@ import React, { createContext, useEffect, useReducer, useState } from 'react'; -import ReactDOM from 'react-dom'; +//import ReactDOM from 'react-dom'; import Voice from './Voice'; import Menu from './Menu'; import { ipcRenderer, remote } from 'electron'; @@ -21,7 +21,7 @@ export const SettingsContext = createContext<[ISettings, React.Dispatch<{ action: ISettings | [string, any]; }>]>(null as any); -function App() { +export default function App() { const [state, setState] = useState(AppState.MENU); const [gameState, setGameState] = useState({} as AmongUsState); const [settingsOpen, setSettingsOpen] = useState(false); @@ -41,13 +41,23 @@ function App() { hideCode: false, stereoInLobby: true }); + - useEffect(() => { + useEffect(() => { const onOpen = (_: Electron.IpcRendererEvent, isOpen: boolean) => { setState(isOpen ? AppState.VOICE : AppState.MENU); + let overlay = remote.getGlobal('overlay'); + if (overlay) { + overlay.webContents.send('overlayState', 'MENU'); + } + }; const onState = (_: Electron.IpcRendererEvent, newState: AmongUsState) => { setGameState(newState); + let overlay = remote.getGlobal('overlay'); + if (overlay) { + overlay.webContents.send('overlayGameState', newState); + } }; let shouldInit = true; const onError = (_: Electron.IpcRendererEvent, error: string) => { @@ -104,4 +114,4 @@ function App() { ) } -ReactDOM.render(, document.getElementById('app')); \ No newline at end of file +//ReactDOM.render(, document.getElementById('app')); \ No newline at end of file diff --git a/src/renderer/Overlay.tsx b/src/renderer/Overlay.tsx new file mode 100644 index 00000000..18b63a2e --- /dev/null +++ b/src/renderer/Overlay.tsx @@ -0,0 +1,160 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { ipcRenderer, remote } from 'electron'; +import { AmongUsState, GameState, Player } from '../main/GameReader'; +import Avatar from './Avatar'; + +interface OtherTalking { + [playerId: number]: boolean; // isTalking +} + +interface OtherDead { + [playerId: number]: boolean; // isTalking +} + +export default function Overlay() { + const [status, setStatus] = useState("WAITING"); + const [gameState, setGameState] = useState({} as AmongUsState); + const [talking, setTalking] = useState(false); + const [otherTalking, setOtherTalking] = useState({}); + const myPlayer = useMemo(() => { + if (!gameState || !gameState.players) return undefined; + else return gameState.players.find(p => p.isLocal); + }, [gameState]); + + const otherPlayers = useMemo(() => { + let otherPlayers: Player[]; + if (!gameState || !gameState.players || gameState.lobbyCode === 'MENU' || !myPlayer) otherPlayers = []; + else otherPlayers = gameState.players.filter(p => !p.isLocal); + + return otherPlayers; + }, [gameState]); + + const talkingPlayers = useMemo(() => { + let talkingPlayers: Player[]; + if (!gameState || !gameState.players || gameState.lobbyCode === 'MENU' || !myPlayer) talkingPlayers = []; + else talkingPlayers = gameState.players.filter(p => (otherTalking[p.id] || (p.isLocal && talking))); + return talkingPlayers; + }, [gameState]); + + const [otherDead, setOtherDead] = useState({}); + + useEffect(() => { + if (gameState.gameState === GameState.LOBBY) { + setOtherDead({}); + } else if (gameState.gameState !== GameState.TASKS) { + if (!gameState.players) return; + setOtherDead(old => { + for (let player of gameState.players) { + old[player.id] = player.isDead || player.disconnected; + } + return { ...old }; + }); + } + }, [gameState.gameState]); + + useEffect(() => { + const onOverlayState = (_: Electron.IpcRendererEvent, state: string) => { + setStatus(state); + }; + + const onOverlayGameState = (_: Electron.IpcRendererEvent, newState: AmongUsState) => { + setGameState(newState); + }; + + const onOverlayTalkingSelf = (_: Electron.IpcRendererEvent, talking: bool) => { + setTalking(talking); + }; + + const onOverlayTalking = (_: Electron.IpcRendererEvent, id: number) => { + setOtherTalking(old => ({ + ...old, + [id]: true + })); + }; + + const onOverlayNotTalking = (_: Electron.IpcRendererEvent, id: number) => { + setOtherTalking(old => ({ + ...old, + [id]: false + })); + + }; + + ipcRenderer.on('overlayState', onOverlayState); + ipcRenderer.on('overlayGameState', onOverlayGameState); + ipcRenderer.on('overlayTalkingSelf', onOverlayTalkingSelf); + ipcRenderer.on('overlayTalking', onOverlayTalking); + ipcRenderer.on('overlayNotTalking', onOverlayNotTalking); + return () => { + ipcRenderer.off('overlayState', onOverlayState); + ipcRenderer.off('overlayGameState', onOverlayGameState); + ipcRenderer.off('overlayTalkingSelf', onOverlayTalkingSelf); + ipcRenderer.off('overlayTalking', onOverlayTalking); + ipcRenderer.off('overlayNotTalking', onOverlayNotTalking); + } + }, []); + + var extra:string = ""; + var myPlayerDisplay:string = "" + if (gameState.gameState == GameState.LOBBY) { + extra =

"State is Lobby."

+ } + + if (myPlayer != undefined) { + var connected = true; + var deafenedState = false; + //extra = "(" + myPlayer.name + ") " + extra; + extra = ""; + + } + + document.body.style.backgroundColor = "rgba(255, 255, 255, 0)"; + const mystyle = { + backgroundColor: "rgba(0, 0, 0, 0.85)", + margin: "10px", + paddingLeft: "8px", + width: "100px", + borderRadius: "8px" + }; + + var topArea =

CrewLink ({status})

+ var playerList = []; + if (gameState.players) playerList = gameState.players; + if (gameState.gameState != GameState.MENU && gameState.gameState != GameState.LOBBY) { + topArea = ""; + playerList = talkingPlayers; + } + + var playerArea = ""; + if (gameState.players) { + playerArea =
+ { + playerList.map(player => { + let connected = true; + return ( +
+ + {player.name} +
+ ); + }) + } +
+ } + + + + return ( +
+ {topArea} + {extra} +
+ {playerArea} +
+
+ ) +} \ No newline at end of file diff --git a/src/renderer/ViewManager.tsx b/src/renderer/ViewManager.tsx new file mode 100644 index 00000000..70036ef2 --- /dev/null +++ b/src/renderer/ViewManager.tsx @@ -0,0 +1,52 @@ +import React, { Component } from 'react'; +// @ts-ignore +import { BrowserRouter as Router, Route } from 'react-router-dom' +import { ipcRenderer } from 'electron'; +import ReactDOM from 'react-dom'; +import App from './App'; +import Overlay from './Overlay'; + +// @ts-ignore +class ViewManager extends Component { + + static Views() { + // @ts-ignore + var val:any = { + // @ts-ignore + app: , + // @ts-ignore + overlay: + } + return val + } + + static View(props: any) { + let name = props.location.search.split("view=")[1]; + // @ts-ignore + console.log("View type: " + name); + let view = ViewManager.Views()[name]; + if(view == null) + + throw new Error("View '" + name + "' is undefined"); + //console.log("View is not null"); + if (name == "app") { + ipcRenderer.send('start'); + console.log("Sent ipcRenderer start"); + } + return view; + } + + render() { + return ( + +
+ +
+
+ ); + } +} + +export default ViewManager + +ReactDOM.render(, document.getElementById('app')); \ No newline at end of file diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index 8c2a874e..d3de3bfe 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -188,8 +188,20 @@ export default function Voice() { const ac = new AudioContext(); ac.createMediaStreamSource(stream) audioListener = VAD(ac, ac.createMediaStreamSource(stream), undefined, { - onVoiceStart: () => setTalking(true), - onVoiceStop: () => setTalking(false), + onVoiceStart: () => { + setTalking(true) + let overlay = remote.getGlobal("overlay"); + if (overlay) { + overlay.webContents.send('overlayTalkingSelf', true); + } + }, + onVoiceStop: () => { + setTalking(false) + let overlay = remote.getGlobal("overlay"); + if (overlay) { + overlay.webContents.send('overlayTalkingSelf', false); + } + }, // onUpdate: console.log, noiseCaptureDuration: 1, }); @@ -209,7 +221,12 @@ export default function Voice() { disconnectPeer(k); }); setSocketPlayerIds({}); - + + let overlay = remote.getGlobal("overlay"); + if (overlay) { + overlay.webContents.send('overlayState', (lobbyCode === 'MENU' ? "MENU" : "VOICE: " + lobbyCode)); + } + if (lobbyCode === 'MENU') return; function disconnectPeer(peer: string) { @@ -278,6 +295,13 @@ export default function Voice() { ...old, [socketPlayerIds[peer]]: talking && gain.gain.value > 0 })); + + let overlay = remote.getGlobal("overlay"); + if (overlay) { + var reallyTalking = talking && gain.gain.value > 0; + overlay.webContents.send(reallyTalking ? 'overlayTalking' : 'overlayNotTalking', socketPlayerIds[peer]); + } + return socketPlayerIds; }); }; diff --git a/src/renderer/css/index.css b/src/renderer/css/index.css index f09f29cb..78396a10 100644 --- a/src/renderer/css/index.css +++ b/src/renderer/css/index.css @@ -100,6 +100,20 @@ body { margin: 5px; } +.otherplayers-overlay { + display: flex; + flex-direction: row; + justify-content: center; + align-items: left; + width: '100%'; + flex-wrap: wrap; + text-align: center; +} + +.otherplayers-overlay>* { + margin: 5px; +} + .top { display: flex; justify-content: center; diff --git a/src/renderer/index.ts b/src/renderer/index.ts index 32b03a24..403b398c 100644 --- a/src/renderer/index.ts +++ b/src/renderer/index.ts @@ -1,5 +1,6 @@ import { ipcRenderer } from 'electron'; -import './App'; +//import './App'; +import './ViewManager'; import './css/index.css'; -ipcRenderer.send('start'); \ No newline at end of file +//ipcRenderer.send('start'); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index bbacc8a5..2b8ae979 100644 --- a/yarn.lock +++ b/yarn.lock @@ -882,6 +882,13 @@ "@babel/plugin-transform-react-jsx-source" "^7.12.1" "@babel/plugin-transform-react-pure-annotations" "^7.12.1" +"@babel/runtime@^7.1.2", "@babel/runtime@^7.12.1": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" + integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/runtime@^7.8.4": version "7.9.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06" @@ -1417,7 +1424,7 @@ ajv@^6.1.0, ajv@^6.10.1, ajv@^6.10.2, ajv@^6.12.0: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^6.12.2, ajv@^6.12.3, ajv@^6.9.1: +ajv@^6.12.2, ajv@^6.12.3, ajv@^6.12.6, ajv@^6.9.1: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -3278,6 +3285,14 @@ electron-devtools-installer@^2.2.4: rimraf "^2.5.2" semver "^5.3.0" +electron-overlay-window@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/electron-overlay-window/-/electron-overlay-window-1.0.4.tgz#58faccaa5aa2dc6effaf522fadc6baa69e7218e3" + integrity sha512-kKXuh5Sq3aFHETkYCVM882/WTtTHV8JbOdXoyc6lg9t+u/cFyvABrt1bMk5f1D/ExAPrmJktPpW6Vj4YhhN6YQ== + dependencies: + node-gyp-build "4.x.x" + throttle-debounce "2.x.x" + electron-publish@22.9.1: version "22.9.1" resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-22.9.1.tgz#7cc76ac4cc53efd29ee31c1e5facb9724329068e" @@ -4577,6 +4592,18 @@ he@^1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== +history@^4.9.0: + version "4.10.1" + resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" + integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== + dependencies: + "@babel/runtime" "^7.1.2" + loose-envify "^1.2.0" + resolve-pathname "^3.0.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + value-equal "^1.0.1" + hmac-drbg@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -4586,6 +4613,13 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" +hoist-non-react-statics@^3.1.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + homedir-polyfill@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" @@ -5600,7 +5634,7 @@ loglevel@^1.6.6: resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.7.tgz#b3e034233188c68b889f5b862415306f565e2c56" integrity sha512-cY2eLFrQSAfVPhCgH1s7JI73tMbg9YC3v3+ZHVW67sBS7UxWzNEk/ZBbSfLykBWHp33dqqtOv82gjhKEi81T/A== -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -5897,6 +5931,14 @@ mimic-response@^2.0.0: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== +mini-create-react-context@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz#072171561bfdc922da08a60c2197a497cc2d1d5e" + integrity sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ== + dependencies: + "@babel/runtime" "^7.12.1" + tiny-warning "^1.0.3" + mini-css-extract-plugin@^0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz#47f2cf07aa165ab35733b1fc97d4c46c0564339e" @@ -6115,6 +6157,11 @@ node-forge@0.9.0: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579" integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ== +node-gyp-build@4.x.x: + version "4.2.3" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.2.3.tgz#ce6277f853835f718829efb47db20f3e4d9c4739" + integrity sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg== + node-libs-browser@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" @@ -6662,6 +6709,13 @@ path-to-regexp@0.1.7: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= +path-to-regexp@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" + integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + dependencies: + isarray "0.0.1" + path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -7127,11 +7181,40 @@ react-dom@^17.0.1: object-assign "^4.1.1" scheduler "^0.20.1" -react-is@^16.6.0, react-is@^16.8.1: +react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-router-dom@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662" + integrity sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA== + dependencies: + "@babel/runtime" "^7.1.2" + history "^4.9.0" + loose-envify "^1.3.1" + prop-types "^15.6.2" + react-router "5.2.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + +react-router@5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.0.tgz#424e75641ca8747fbf76e5ecca69781aa37ea293" + integrity sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw== + dependencies: + "@babel/runtime" "^7.1.2" + history "^4.9.0" + hoist-non-react-statics "^3.1.0" + loose-envify "^1.3.1" + mini-create-react-context "^0.4.0" + path-to-regexp "^1.7.0" + prop-types "^15.6.2" + react-is "^16.6.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + react-spinners-kit@^1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/react-spinners-kit/-/react-spinners-kit-1.9.1.tgz#516a7de8e80702def006be481062382ab3c38066" @@ -7486,6 +7569,11 @@ resolve-from@^4.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== +resolve-pathname@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" + integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== + resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" @@ -8503,6 +8591,11 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= +throttle-debounce@2.x.x: + version "2.3.0" + resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.3.0.tgz#fd31865e66502071e411817e241465b3e9c372e2" + integrity sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ== + throttleit@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-0.0.2.tgz#cfedf88e60c00dd9697b61fdd2a8343a9b680eaf" @@ -8541,6 +8634,16 @@ timers-browserify@^2.0.4: dependencies: setimmediate "^1.0.4" +tiny-invariant@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" + integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== + +tiny-warning@^1.0.0, tiny-warning@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -8959,6 +9062,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +value-equal@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" + integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== + vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" From f55b765f8e0034f4968a200bf9a46d12cf7c8d1e Mon Sep 17 00:00:00 2001 From: Ryan Shavell Date: Thu, 10 Dec 2020 12:00:27 -0500 Subject: [PATCH 07/88] Better visuals, better positioning, hide dead people when you're alive, some typescript fixes --- src/main/index.ts | 18 +++++++-- src/renderer/App.tsx | 3 +- src/renderer/Overlay.tsx | 82 ++++++++++++++++++++++++-------------- src/renderer/Settings.tsx | 12 ++++++ src/renderer/Voice.tsx | 2 +- src/renderer/css/index.css | 13 ------ src/renderer/index.ts | 6 +-- 7 files changed, 84 insertions(+), 52 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index 8944917a..165c95ef 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -11,9 +11,9 @@ import { overlayWindow } from 'electron-overlay-window'; const isDevelopment = process.env.NODE_ENV !== 'production' // global reference to mainWindow (necessary to prevent window from being garbage collected) -//let mainWindow: BrowserWindow | null; -//let overlay: BrowserWindow | null; +// @ts-ignore global.mainWindow = null; +// @ts-ignore global.overlay = null; app.commandLine.appendSwitch('disable-pinch'); @@ -69,7 +69,7 @@ if (!gotTheLock) { } window.on('closed', () => { - mainWindow = null + global.mainWindow = null }) window.webContents.on('devtools-opened', () => { @@ -94,7 +94,19 @@ if (!gotTheLock) { ...overlayWindow.WINDOW_OPTS }); + if (isDevelopment) { overlay.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}?version=${autoUpdater.currentVersion.version}&view=overlay`) + } else { + window.loadURL(formatUrl({ + pathname: path.join(__dirname, 'index.html'), + protocol: 'file', + query: { + version: autoUpdater.currentVersion.version, + view: "overlay" + }, + slashes: true + })) + } //overlay.webContents.openDevTools() overlay.setIgnoreMouseEvents(true); overlayWindow.attachTo(overlay, 'Among Us') diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 519aa46b..c165102c 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -39,7 +39,8 @@ export default function App() { data: '' }, hideCode: false, - stereoInLobby: true + stereoInLobby: true, + compactOverlay: false }); diff --git a/src/renderer/Overlay.tsx b/src/renderer/Overlay.tsx index 18b63a2e..bcfd0fb9 100644 --- a/src/renderer/Overlay.tsx +++ b/src/renderer/Overlay.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useMemo, useState } from 'react'; -import { ipcRenderer, remote } from 'electron'; +import { ipcRenderer } from 'electron'; import { AmongUsState, GameState, Player } from '../main/GameReader'; import Avatar from './Avatar'; @@ -38,6 +38,13 @@ export default function Overlay() { const [otherDead, setOtherDead] = useState({}); + const relevantPlayers = useMemo(() => { + let relevantPlayers: Player[]; + if (!gameState || !gameState.players || gameState.lobbyCode === 'MENU' || !myPlayer) relevantPlayers = []; + else relevantPlayers = gameState.players.filter(p => ((!myPlayer.isDead && !otherDead[p.id]) || myPlayer.isDead)); + return relevantPlayers; + }, [gameState]); + useEffect(() => { if (gameState.gameState === GameState.LOBBY) { setOtherDead({}); @@ -61,7 +68,7 @@ export default function Overlay() { setGameState(newState); }; - const onOverlayTalkingSelf = (_: Electron.IpcRendererEvent, talking: bool) => { + const onOverlayTalkingSelf = (_: Electron.IpcRendererEvent, talking: boolean) => { setTalking(talking); }; @@ -94,52 +101,69 @@ export default function Overlay() { } }, []); - var extra:string = ""; - var myPlayerDisplay:string = "" + // TODO: access settings and read settings.compactOverlay + var compact = false; + + var extra:JSX.Element = <>; if (gameState.gameState == GameState.LOBBY) { extra =

"State is Lobby."

} if (myPlayer != undefined) { - var connected = true; - var deafenedState = false; - //extra = "(" + myPlayer.name + ") " + extra; - extra = ""; + extra = <>; } document.body.style.backgroundColor = "rgba(255, 255, 255, 0)"; - const mystyle = { + document.body.style.paddingTop = "0"; + var baseCSS:any = { backgroundColor: "rgba(0, 0, 0, 0.85)", - margin: "10px", - paddingLeft: "8px", width: "100px", - borderRadius: "8px" + borderRadius: "8px", + position: "relative", + marginTop: "-16px", + paddingLeft: "8px", }; + var topArea =

CrewLink ({status})

+ var playerList:Player[] = []; + if (gameState.players && gameState.gameState != GameState.MENU) playerList = relevantPlayers;//gameState.players; - var topArea =

CrewLink ({status})

- var playerList = []; - if (gameState.players) playerList = gameState.players; - if (gameState.gameState != GameState.MENU && gameState.gameState != GameState.LOBBY) { - topArea = ""; - playerList = talkingPlayers; + if (gameState.gameState == GameState.UNKNOWN || gameState.gameState == GameState.MENU) { + baseCSS["left"] = "8px"; + baseCSS["top"] = "60px"; + } else { + baseCSS["marginLeft"] = "auto"; + baseCSS["marginRight"] = "auto"; + baseCSS["marginTop"] = "0px"; + baseCSS["paddingTop"] = "8px"; + baseCSS["paddingLeft"] = "0px"; + baseCSS["width"] = "800px"; + baseCSS["backgroundColor"] = "rgba(0, 0, 0, 0.5)"; //0.25 + topArea = <>; + if ((compact || gameState.gameState == GameState.TASKS) && playerList) { + playerList = talkingPlayers; + baseCSS["backgroundColor"] = "rgba(0, 0, 0, 0)"; + } } - var playerArea = ""; - if (gameState.players) { - playerArea =
+ var playerArea:JSX.Element = <>; + if (playerList) { + playerArea =
{ playerList.map(player => { let connected = true; + let name = compact ? "" : {player.name} return ( -
- - {player.name} +
+
+
+ {name} +
); }) } @@ -149,7 +173,7 @@ export default function Overlay() { return ( -
+
{topArea} {extra}
diff --git a/src/renderer/Settings.tsx b/src/renderer/Settings.tsx index 47580ad4..9214429e 100644 --- a/src/renderer/Settings.tsx +++ b/src/renderer/Settings.tsx @@ -89,6 +89,10 @@ const store = new Store({ stereoInLobby: { type: 'boolean', default: true + }, + compactOverlay: { + type: 'boolean', + default: false } } }); @@ -118,6 +122,7 @@ export interface ISettings { }, hideCode: boolean; stereoInLobby: boolean; + compactOverlay: boolean; } export const settingsReducer = (state: ISettings, action: { type: 'set' | 'setOne', action: [string, any] | ISettings @@ -326,6 +331,13 @@ export default function Settings({ open, onClose }: SettingsProps) {
+
setSettings({ + type: 'setOne', + action: ['compactOverlay', !settings.compactOverlay] + })}> + + +
} \ No newline at end of file diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index d3de3bfe..a8ef47cc 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -224,7 +224,7 @@ export default function Voice() { let overlay = remote.getGlobal("overlay"); if (overlay) { - overlay.webContents.send('overlayState', (lobbyCode === 'MENU' ? "MENU" : "VOICE: " + lobbyCode)); + overlay.webContents.send('overlayState', (lobbyCode === 'MENU' ? "MENU" : "VOICE")); } if (lobbyCode === 'MENU') return; diff --git a/src/renderer/css/index.css b/src/renderer/css/index.css index 78396a10..f3cc2e1d 100644 --- a/src/renderer/css/index.css +++ b/src/renderer/css/index.css @@ -100,19 +100,6 @@ body { margin: 5px; } -.otherplayers-overlay { - display: flex; - flex-direction: row; - justify-content: center; - align-items: left; - width: '100%'; - flex-wrap: wrap; - text-align: center; -} - -.otherplayers-overlay>* { - margin: 5px; -} .top { display: flex; diff --git a/src/renderer/index.ts b/src/renderer/index.ts index 403b398c..291b8609 100644 --- a/src/renderer/index.ts +++ b/src/renderer/index.ts @@ -1,6 +1,2 @@ -import { ipcRenderer } from 'electron'; -//import './App'; import './ViewManager'; -import './css/index.css'; - -//ipcRenderer.send('start'); \ No newline at end of file +import './css/index.css'; \ No newline at end of file From 085ff9d5f32845fb2aefa9ca3eff39a925b59f4d Mon Sep 17 00:00:00 2001 From: Ryan Shavell Date: Thu, 10 Dec 2020 15:51:12 -0500 Subject: [PATCH 08/88] cleanup, appease TypeScript --- package.json | 4 ++-- src/main/index.ts | 16 +++++++++++----- src/renderer/App.tsx | 1 - src/renderer/Overlay.tsx | 8 ++++---- src/renderer/ViewManager.tsx | 1 - src/renderer/css/index.css | 5 ++++- 6 files changed, 21 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index dbba33c7..b4bfa4ec 100644 --- a/package.json +++ b/package.json @@ -32,13 +32,13 @@ "cross-spawn": "^7.0.3", "electron-store": "^6.0.1", "electron-updater": "^4.3.5", - "electron-overlay-window": "^1.0.4", + "electron-overlay-window": "^1.0.4", "iohook": "git://github.com/ykhwong/iohook", "jsondiffpatch": "^0.4.1", "memoryjs": "https://github.com/Rob--/memoryjs", "react": "^17.0.1", "react-dom": "^17.0.1", - "react-router-dom": "^5.2.0", + "react-router-dom": "^5.2.0", "react-spinners-kit": "^1.9.1", "react-tooltip-lite": "^1.12.0", "registry-js": "^1.12.0", diff --git a/src/main/index.ts b/src/main/index.ts index 165c95ef..62149626 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -7,13 +7,18 @@ import { format as formatUrl } from 'url' import './hook'; import { overlayWindow } from 'electron-overlay-window'; - const isDevelopment = process.env.NODE_ENV !== 'production' +declare global { + namespace NodeJS { + interface Global { + mainWindow: BrowserWindow|null; + overlay: BrowserWindow|null; + } + } +} // global reference to mainWindow (necessary to prevent window from being garbage collected) -// @ts-ignore global.mainWindow = null; -// @ts-ignore global.overlay = null; app.commandLine.appendSwitch('disable-pinch'); @@ -95,9 +100,10 @@ if (!gotTheLock) { }); if (isDevelopment) { - overlay.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}?version=${autoUpdater.currentVersion.version}&view=overlay`) + overlay.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}?version=${autoUpdater.currentVersion.version}&view=overlay`) } else { - window.loadURL(formatUrl({ + + overlay.loadURL(formatUrl({ pathname: path.join(__dirname, 'index.html'), protocol: 'file', query: { diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index c165102c..95abb8ff 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1,5 +1,4 @@ import React, { createContext, useEffect, useReducer, useState } from 'react'; -//import ReactDOM from 'react-dom'; import Voice from './Voice'; import Menu from './Menu'; import { ipcRenderer, remote } from 'electron'; diff --git a/src/renderer/Overlay.tsx b/src/renderer/Overlay.tsx index bcfd0fb9..506a2acb 100644 --- a/src/renderer/Overlay.tsx +++ b/src/renderer/Overlay.tsx @@ -4,11 +4,11 @@ import { AmongUsState, GameState, Player } from '../main/GameReader'; import Avatar from './Avatar'; interface OtherTalking { - [playerId: number]: boolean; // isTalking + [playerId: number]: boolean; } interface OtherDead { - [playerId: number]: boolean; // isTalking + [playerId: number]: boolean; } export default function Overlay() { @@ -21,13 +21,13 @@ export default function Overlay() { else return gameState.players.find(p => p.isLocal); }, [gameState]); - const otherPlayers = useMemo(() => { + /*const otherPlayers = useMemo(() => { let otherPlayers: Player[]; if (!gameState || !gameState.players || gameState.lobbyCode === 'MENU' || !myPlayer) otherPlayers = []; else otherPlayers = gameState.players.filter(p => !p.isLocal); return otherPlayers; - }, [gameState]); + }, [gameState]);*/ const talkingPlayers = useMemo(() => { let talkingPlayers: Player[]; diff --git a/src/renderer/ViewManager.tsx b/src/renderer/ViewManager.tsx index 70036ef2..e413c503 100644 --- a/src/renderer/ViewManager.tsx +++ b/src/renderer/ViewManager.tsx @@ -26,7 +26,6 @@ class ViewManager extends Component { console.log("View type: " + name); let view = ViewManager.Views()[name]; if(view == null) - throw new Error("View '" + name + "' is undefined"); //console.log("View is not null"); if (name == "app") { diff --git a/src/renderer/css/index.css b/src/renderer/css/index.css index f3cc2e1d..d2c9227f 100644 --- a/src/renderer/css/index.css +++ b/src/renderer/css/index.css @@ -94,13 +94,16 @@ body { align-items: center; width: '100%'; flex-wrap: wrap; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .otherplayers>* { margin: 5px; } - .top { display: flex; justify-content: center; From 43394467a1f777a0ceb1ef06981faa2feebc3364 Mon Sep 17 00:00:00 2001 From: Ryan Shavell Date: Fri, 11 Dec 2020 17:51:24 -0500 Subject: [PATCH 09/88] Fix formatting, start working on settings --- src/renderer/Overlay.tsx | 4 +++- src/renderer/css/index.css | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/renderer/Overlay.tsx b/src/renderer/Overlay.tsx index 506a2acb..c56d1db4 100644 --- a/src/renderer/Overlay.tsx +++ b/src/renderer/Overlay.tsx @@ -1,7 +1,8 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState, useContext } from 'react'; import { ipcRenderer } from 'electron'; import { AmongUsState, GameState, Player } from '../main/GameReader'; import Avatar from './Avatar'; +//import { SettingsContext } from "./App"; interface OtherTalking { [playerId: number]: boolean; @@ -20,6 +21,7 @@ export default function Overlay() { if (!gameState || !gameState.players) return undefined; else return gameState.players.find(p => p.isLocal); }, [gameState]); + //const [settings] = useContext(SettingsContext); /*const otherPlayers = useMemo(() => { let otherPlayers: Player[]; diff --git a/src/renderer/css/index.css b/src/renderer/css/index.css index d2c9227f..b29089e8 100644 --- a/src/renderer/css/index.css +++ b/src/renderer/css/index.css @@ -96,8 +96,8 @@ body { flex-wrap: wrap; white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + overflow: hidden; + text-overflow: ellipsis; } .otherplayers>* { From dee1665646da8bbf7efea3b090b3d6554c1d74f9 Mon Sep 17 00:00:00 2001 From: Ryan Shavell Date: Fri, 11 Dec 2020 18:02:42 -0500 Subject: [PATCH 10/88] Fix haunting setting definition --- src/common/ISettings.d.ts | 1 + yarn.lock | 5 ----- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/common/ISettings.d.ts b/src/common/ISettings.d.ts index e54031a3..5cfece3b 100644 --- a/src/common/ISettings.d.ts +++ b/src/common/ISettings.d.ts @@ -13,4 +13,5 @@ export interface ISettings { }, hideCode: boolean; enableSpatialAudio: boolean; + haunting: boolean; } diff --git a/yarn.lock b/yarn.lock index 3193fa5a..57ab37f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1844,11 +1844,6 @@ atomically@^1.3.1: resolved "https://registry.yarnpkg.com/atomically/-/atomically-1.6.0.tgz#d8d47f99834dbb88bd6266cc69a1447e2f3675ec" integrity sha512-mu394MH+yY2TSKMyH+978PcGMZ8sRNks2PuVeH6c2ED4mimR2LEE039MVcIGVhtmG54cKEMh4gKhxKL/CLaX/w== -audio-activity@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/audio-activity/-/audio-activity-1.0.0.tgz#f653b9668582e7fd6a4c9b9997abe26607bcc456" - integrity sha1-9lO5ZoWC5/1qTJuZl6viZge8xFY= - audio-frequency-to-index@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/audio-frequency-to-index/-/audio-frequency-to-index-2.0.0.tgz#4c4bca9f3bfec38c773aa6b5604c117adc982d45" From f2ad1433197bee109aca03f844f23533222bd70e Mon Sep 17 00:00:00 2001 From: Ryan Shavell Date: Fri, 11 Dec 2020 19:08:39 -0500 Subject: [PATCH 11/88] Overlay can read settings, finish merge --- src/common/ISettings.d.ts | 1 + src/main/index.ts | 8 ++++---- src/renderer/App.tsx | 3 +-- src/renderer/Overlay.tsx | 36 +++++++++++++++++++----------------- src/renderer/Settings.tsx | 6 ++++++ yarn.lock | 5 ----- 6 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/common/ISettings.d.ts b/src/common/ISettings.d.ts index e54031a3..60401a83 100644 --- a/src/common/ISettings.d.ts +++ b/src/common/ISettings.d.ts @@ -13,4 +13,5 @@ export interface ISettings { }, hideCode: boolean; enableSpatialAudio: boolean; + compactOverlay: boolean; } diff --git a/src/main/index.ts b/src/main/index.ts index d39605fa..30c05a0d 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -52,7 +52,7 @@ function createMainWindow() { } if (isDevelopment) { - window.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}?version=${autoUpdater.currentVersion.version}`); + window.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}?version=${autoUpdater.currentVersion.version}&view=app`); } else { window.loadURL(formatUrl({ @@ -66,7 +66,7 @@ function createMainWindow() { } window.on('closed', () => { - mainWindow = null; + global.mainWindow = null; }); window.webContents.on('devtools-opened', () => { @@ -109,7 +109,7 @@ if (!gotTheLock) { } else { overlay.loadURL(formatUrl({ - pathname: path.join(__dirname, 'index.html'), + pathname: joinPath(__dirname, 'index.html'), protocol: 'file', query: { version: autoUpdater.currentVersion.version, @@ -119,9 +119,9 @@ if (!gotTheLock) { })) } //overlay.webContents.openDevTools() + //overlayWindow.attachTo(overlay, 'Untitled - Notepad') overlay.setIgnoreMouseEvents(true); overlayWindow.attachTo(overlay, 'Among Us') - //overlayWindow.attachTo(overlay, 'Untitled - Notepad') return overlay; } diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 1ae5f147..9b716c76 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1,5 +1,4 @@ -import React, { createContext, useEffect, useReducer, useState } from 'react'; -import ReactDOM from 'react-dom'; +import React, { useEffect, useReducer, useState } from 'react'; import Voice from './Voice'; import Menu from './Menu'; import { ipcRenderer, remote } from 'electron'; diff --git a/src/renderer/Overlay.tsx b/src/renderer/Overlay.tsx index c56d1db4..2de84647 100644 --- a/src/renderer/Overlay.tsx +++ b/src/renderer/Overlay.tsx @@ -1,8 +1,8 @@ -import React, { useEffect, useMemo, useState, useContext } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { ipcRenderer } from 'electron'; -import { AmongUsState, GameState, Player } from '../main/GameReader'; +import { GameState, AmongUsState, Player } from '../common/AmongUsState'; import Avatar from './Avatar'; -//import { SettingsContext } from "./App"; +import { ISettings } from '../common/ISettings'; interface OtherTalking { [playerId: number]: boolean; @@ -21,25 +21,17 @@ export default function Overlay() { if (!gameState || !gameState.players) return undefined; else return gameState.players.find(p => p.isLocal); }, [gameState]); - //const [settings] = useContext(SettingsContext); - + const [settings, setSettings] = useState({} as ISettings); + const [otherDead, setOtherDead] = useState({}); /*const otherPlayers = useMemo(() => { let otherPlayers: Player[]; if (!gameState || !gameState.players || gameState.lobbyCode === 'MENU' || !myPlayer) otherPlayers = []; else otherPlayers = gameState.players.filter(p => !p.isLocal); return otherPlayers; - }, [gameState]);*/ + }, [gameState]);*/ - const talkingPlayers = useMemo(() => { - let talkingPlayers: Player[]; - if (!gameState || !gameState.players || gameState.lobbyCode === 'MENU' || !myPlayer) talkingPlayers = []; - else talkingPlayers = gameState.players.filter(p => (otherTalking[p.id] || (p.isLocal && talking))); - return talkingPlayers; - }, [gameState]); - const [otherDead, setOtherDead] = useState({}); - const relevantPlayers = useMemo(() => { let relevantPlayers: Player[]; if (!gameState || !gameState.players || gameState.lobbyCode === 'MENU' || !myPlayer) relevantPlayers = []; @@ -47,6 +39,10 @@ export default function Overlay() { return relevantPlayers; }, [gameState]); + let talkingPlayers: Player[]; + if (!gameState || !gameState.players || gameState.lobbyCode === 'MENU' || !myPlayer) talkingPlayers = []; + else talkingPlayers = gameState.players.filter(p => (otherTalking[p.id] || (p.isLocal && talking))); + useEffect(() => { if (gameState.gameState === GameState.LOBBY) { setOtherDead({}); @@ -61,7 +57,11 @@ export default function Overlay() { } }, [gameState.gameState]); - useEffect(() => { + useEffect(() => { + const onOverlaySettings = (_: Electron.IpcRendererEvent, newSettings: any) => { + setSettings(newSettings); + }; + const onOverlayState = (_: Electron.IpcRendererEvent, state: string) => { setStatus(state); }; @@ -70,6 +70,7 @@ export default function Overlay() { setGameState(newState); }; + const onOverlayTalkingSelf = (_: Electron.IpcRendererEvent, talking: boolean) => { setTalking(talking); }; @@ -89,12 +90,14 @@ export default function Overlay() { }; + ipcRenderer.on('overlaySettings', onOverlaySettings); ipcRenderer.on('overlayState', onOverlayState); ipcRenderer.on('overlayGameState', onOverlayGameState); ipcRenderer.on('overlayTalkingSelf', onOverlayTalkingSelf); ipcRenderer.on('overlayTalking', onOverlayTalking); ipcRenderer.on('overlayNotTalking', onOverlayNotTalking); return () => { + ipcRenderer.off('overlaySettings', onOverlaySettings); ipcRenderer.off('overlayState', onOverlayState); ipcRenderer.off('overlayGameState', onOverlayGameState); ipcRenderer.off('overlayTalkingSelf', onOverlayTalkingSelf); @@ -103,8 +106,7 @@ export default function Overlay() { } }, []); - // TODO: access settings and read settings.compactOverlay - var compact = false; + var compact = settings.compactOverlay; var extra:JSX.Element = <>; if (gameState.gameState == GameState.LOBBY) { diff --git a/src/renderer/Settings.tsx b/src/renderer/Settings.tsx index a5248cda..4faf0ccb 100644 --- a/src/renderer/Settings.tsx +++ b/src/renderer/Settings.tsx @@ -6,6 +6,7 @@ import './css/settings.css'; import MicrophoneSoundBar from './MicrophoneSoundBar'; import TestSpeakersButton from './TestSpeakersButton'; import { ISettings } from '../common/ISettings'; +import { remote } from 'electron'; const keys = new Set(['Space', 'Backspace', 'Delete', 'Enter', 'Up', 'Down', 'Left', 'Right', 'Home', 'End', 'PageUp', 'PageDown', 'Escape', 'LControl', 'LShift', 'LAlt', 'RControl', 'RShift', 'RAlt']); @@ -172,6 +173,11 @@ const Settings: React.FC = function ({ open, onClose }: SettingsP action: store.store }); }, []); + + let overlay = remote.getGlobal('overlay'); + if (overlay) { + overlay.webContents.send('overlaySettings', settings); + } useEffect(() => { setUnsavedCount(s => s + 1); diff --git a/yarn.lock b/yarn.lock index aebaed1f..75385bb7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1851,11 +1851,6 @@ atomically@^1.3.1: resolved "https://registry.yarnpkg.com/atomically/-/atomically-1.6.0.tgz#d8d47f99834dbb88bd6266cc69a1447e2f3675ec" integrity sha512-mu394MH+yY2TSKMyH+978PcGMZ8sRNks2PuVeH6c2ED4mimR2LEE039MVcIGVhtmG54cKEMh4gKhxKL/CLaX/w== -audio-activity@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/audio-activity/-/audio-activity-1.0.0.tgz#f653b9668582e7fd6a4c9b9997abe26607bcc456" - integrity sha1-9lO5ZoWC5/1qTJuZl6viZge8xFY= - audio-frequency-to-index@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/audio-frequency-to-index/-/audio-frequency-to-index-2.0.0.tgz#4c4bca9f3bfec38c773aa6b5604c117adc982d45" From 45e3f6f81e46346df67207974b1ff267461e5587 Mon Sep 17 00:00:00 2001 From: Ryan Shavell Date: Fri, 11 Dec 2020 19:23:48 -0500 Subject: [PATCH 12/88] Cleanup --- src/main/index.ts | 2 +- src/renderer/App.tsx | 6 ++---- src/renderer/Overlay.tsx | 27 ++++++--------------------- src/renderer/Voice.tsx | 1 - src/renderer/css/index.css | 3 +-- 5 files changed, 10 insertions(+), 29 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index 30c05a0d..e48ed0a1 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -137,7 +137,7 @@ if (!gotTheLock) { app.on('activate', () => { // on macOS it is common to re-create a window even after all windows have been closed if (global.mainWindow === null) { - global.mainWindow = createMainWindow() + global.mainWindow = createMainWindow(); } }); diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 9b716c76..5e1ac1a6 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -38,7 +38,7 @@ export default function App() { }); - useEffect(() => { + useEffect(() => { const onOpen = (_: Electron.IpcRendererEvent, isOpen: boolean) => { setState(isOpen ? AppState.VOICE : AppState.MENU); let overlay = remote.getGlobal('overlay'); @@ -107,6 +107,4 @@ export default function App() { ); -} - -//ReactDOM.render(, document.getElementById('app')); \ No newline at end of file +} \ No newline at end of file diff --git a/src/renderer/Overlay.tsx b/src/renderer/Overlay.tsx index 2de84647..9a1a77d5 100644 --- a/src/renderer/Overlay.tsx +++ b/src/renderer/Overlay.tsx @@ -105,19 +105,7 @@ export default function Overlay() { ipcRenderer.off('overlayNotTalking', onOverlayNotTalking); } }, []); - - var compact = settings.compactOverlay; - - var extra:JSX.Element = <>; - if (gameState.gameState == GameState.LOBBY) { - extra =

"State is Lobby."

- } - - if (myPlayer != undefined) { - extra = <>; - - } - + document.body.style.backgroundColor = "rgba(255, 255, 255, 0)"; document.body.style.paddingTop = "0"; var baseCSS:any = { @@ -144,7 +132,7 @@ export default function Overlay() { baseCSS["width"] = "800px"; baseCSS["backgroundColor"] = "rgba(0, 0, 0, 0.5)"; //0.25 topArea = <>; - if ((compact || gameState.gameState == GameState.TASKS) && playerList) { + if ((settings.compactOverlay || gameState.gameState == GameState.TASKS) && playerList) { playerList = talkingPlayers; baseCSS["backgroundColor"] = "rgba(0, 0, 0, 0)"; } @@ -153,24 +141,24 @@ export default function Overlay() { var playerArea:JSX.Element = <>; if (playerList) { playerArea =
- { + { playerList.map(player => { let connected = true; - let name = compact ? "" : {player.name} + let name = settings.compactOverlay ? "" : {player.name} return (
{name}
); }) - } + }
} @@ -179,10 +167,7 @@ export default function Overlay() { return (
{topArea} - {extra} -
{playerArea} -
) } \ No newline at end of file diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index 3f10bd59..fbeaca75 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -209,7 +209,6 @@ const Voice: React.FC = function () { overlay.webContents.send('overlayTalkingSelf', false); } }, - // onUpdate: console.log, noiseCaptureDuration: 1, stereo: false }); diff --git a/src/renderer/css/index.css b/src/renderer/css/index.css index f9ecf689..86aaf820 100644 --- a/src/renderer/css/index.css +++ b/src/renderer/css/index.css @@ -94,8 +94,7 @@ body { justify-content: center; align-items: center; width: '100%'; - flex-wrap: wrap; - + flex-wrap: wrap; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; From f567712318683eb1aff09cb68b05c1d9a39b5119 Mon Sep 17 00:00:00 2001 From: Ryan Shavell Date: Fri, 11 Dec 2020 20:40:40 -0500 Subject: [PATCH 13/88] Hide users who aren't connected to CrewLink --- src/renderer/Overlay.tsx | 25 ++++++++++++++++++++----- src/renderer/Voice.tsx | 12 +++++++----- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/renderer/Overlay.tsx b/src/renderer/Overlay.tsx index 9a1a77d5..e7555ee7 100644 --- a/src/renderer/Overlay.tsx +++ b/src/renderer/Overlay.tsx @@ -12,17 +12,23 @@ interface OtherDead { [playerId: number]: boolean; } +interface SocketIdMap { + [socketId: string]: number; +} + export default function Overlay() { const [status, setStatus] = useState("WAITING"); const [gameState, setGameState] = useState({} as AmongUsState); + const [settings, setSettings] = useState({} as ISettings); + const [socketPlayerIds, setSocketPlayerIds] = useState({}); const [talking, setTalking] = useState(false); const [otherTalking, setOtherTalking] = useState({}); + const [otherDead, setOtherDead] = useState({}); const myPlayer = useMemo(() => { if (!gameState || !gameState.players) return undefined; else return gameState.players.find(p => p.isLocal); }, [gameState]); - const [settings, setSettings] = useState({} as ISettings); - const [otherDead, setOtherDead] = useState({}); + /*const otherPlayers = useMemo(() => { let otherPlayers: Player[]; if (!gameState || !gameState.players || gameState.lobbyCode === 'MENU' || !myPlayer) otherPlayers = []; @@ -35,7 +41,10 @@ export default function Overlay() { const relevantPlayers = useMemo(() => { let relevantPlayers: Player[]; if (!gameState || !gameState.players || gameState.lobbyCode === 'MENU' || !myPlayer) relevantPlayers = []; - else relevantPlayers = gameState.players.filter(p => ((!myPlayer.isDead && !otherDead[p.id]) || myPlayer.isDead)); + else relevantPlayers = gameState.players.filter(p => ( + (Object.values(socketPlayerIds).includes(p.id) || p.isLocal) && + ((!myPlayer.isDead && !otherDead[p.id]) || myPlayer.isDead) + )); return relevantPlayers; }, [gameState]); @@ -70,6 +79,10 @@ export default function Overlay() { setGameState(newState); }; + const onOverlaySocketIds = (_: Electron.IpcRendererEvent, ids: SocketIdMap) => { + setSocketPlayerIds(ids); + }; + const onOverlayTalkingSelf = (_: Electron.IpcRendererEvent, talking: boolean) => { setTalking(talking); @@ -93,6 +106,7 @@ export default function Overlay() { ipcRenderer.on('overlaySettings', onOverlaySettings); ipcRenderer.on('overlayState', onOverlayState); ipcRenderer.on('overlayGameState', onOverlayGameState); + ipcRenderer.on('overlaySocketIds', onOverlaySocketIds); ipcRenderer.on('overlayTalkingSelf', onOverlayTalkingSelf); ipcRenderer.on('overlayTalking', onOverlayTalking); ipcRenderer.on('overlayNotTalking', onOverlayNotTalking); @@ -100,6 +114,7 @@ export default function Overlay() { ipcRenderer.off('overlaySettings', onOverlaySettings); ipcRenderer.off('overlayState', onOverlayState); ipcRenderer.off('overlayGameState', onOverlayGameState); + ipcRenderer.off('overlaySocketIds', onOverlaySocketIds); ipcRenderer.off('overlayTalkingSelf', onOverlayTalkingSelf); ipcRenderer.off('overlayTalking', onOverlayTalking); ipcRenderer.off('overlayNotTalking', onOverlayNotTalking); @@ -143,10 +158,10 @@ export default function Overlay() { playerArea =
{ playerList.map(player => { - let connected = true; + const connected = Object.values(socketPlayerIds).includes(player.id) || player.isLocal; let name = settings.compactOverlay ? "" : {player.name} return ( -
+
0; - overlay.webContents.send(reallyTalking ? 'overlayTalking' : 'overlayNotTalking', socketPlayerIds[peer]); - } + if (overlay) overlay.webContents.send('overlaySocketIds', socketPlayerIds); return socketPlayerIds; }); + let overlay = remote.getGlobal("overlay"); + if (overlay) overlay.webContents.send('overlaySocketIds', socketPlayerIds); }; audioElements.current[peer] = { element: audio, gain, pan }; }); @@ -315,7 +314,8 @@ const Voice: React.FC = function () { } socket.on('join', async (peer: string, playerId: number) => { createPeerConnection(peer, true); - setSocketPlayerIds(old => ({ ...old, [peer]: playerId })); + setSocketPlayerIds(old => ({ ...old, [peer]: playerId })); + }); socket.on('signal', ({ data, from }: { data: Peer.SignalData, from: string }) => { let connection: Peer.Instance; @@ -364,6 +364,8 @@ const Voice: React.FC = function () { for (const k of Object.keys(socketPlayerIds)) { playerSocketIds[socketPlayerIds[k]] = k; } + let overlay = remote.getGlobal("overlay"); + if (overlay) overlay.webContents.send('overlaySocketIds', socketPlayerIds); for (const player of otherPlayers) { const audio = audioElements.current[playerSocketIds[player.id]]; if (audio) { From 4ef6604aea2d88df4cee4718209a3e27926b703a Mon Sep 17 00:00:00 2001 From: Ryan Shavell Date: Fri, 11 Dec 2020 20:48:19 -0500 Subject: [PATCH 14/88] restore voice detection that was deleted by mistake --- src/renderer/Voice.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index 90cfff2b..3768e601 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -295,7 +295,11 @@ const Voice: React.FC = function () { })); let overlay = remote.getGlobal("overlay"); - if (overlay) overlay.webContents.send('overlaySocketIds', socketPlayerIds); + if (overlay) { + var reallyTalking = talking && gain.gain.value > 0; + overlay.webContents.send(reallyTalking ? 'overlayTalking' : 'overlayNotTalking', socketPlayerIds[peer]); + overlay.webContents.send('overlaySocketIds', socketPlayerIds); + } return socketPlayerIds; }); @@ -314,8 +318,7 @@ const Voice: React.FC = function () { } socket.on('join', async (peer: string, playerId: number) => { createPeerConnection(peer, true); - setSocketPlayerIds(old => ({ ...old, [peer]: playerId })); - + setSocketPlayerIds(old => ({ ...old, [peer]: playerId })); }); socket.on('signal', ({ data, from }: { data: Peer.SignalData, from: string }) => { let connection: Peer.Instance; From 7fe554e60467b9733c2adf4a181fce90aac2e81e Mon Sep 17 00:00:00 2001 From: Brian Luo Date: Wed, 9 Dec 2020 21:08:06 -0800 Subject: [PATCH 15/88] Basic muffling (needs slight value adjustments) Co-authored-by: SeanEZhou Co-authored-by: Rong Kang Chew --- src/renderer/Voice.tsx | 32 +++++++++++++++++++++++++------- yarn.lock | 4 ++++ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index ebaf4e08..8e4307a3 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -21,6 +21,7 @@ interface AudioElements { element: HTMLAudioElement; gain: GainNode; pan: PannerNode; + muffle: BiquadFilterNode; }; } @@ -43,7 +44,7 @@ interface OtherDead { [playerId: number]: boolean; // isTalking } -function calculateVoiceAudio(state: AmongUsState, settings: ISettings, me: Player, other: Player, gain: GainNode, pan: PannerNode): void { +function calculateVoiceAudio(state: AmongUsState, settings: ISettings, me: Player, other: Player, gain: GainNode, pan: PannerNode, muffle: BiquadFilterNode): void { const audioContext = pan.context; pan.positionZ.setValueAtTime(-0.5, audioContext.currentTime); let panPos = [ @@ -71,6 +72,17 @@ function calculateVoiceAudio(state: AmongUsState, settings: ISettings, me: Playe gain.gain.value = 0; return; } + + if (me.inVent) { + // Enable muffle + muffle.frequency.value = 400; + muffle.Q.value = 20; + } else { + // Disable muffle + muffle.frequency.value = 20000; + muffle.Q.value = 0; + } + if (state.gameState === GameState.LOBBY || state.gameState === GameState.DISCUSSION) { gain.gain.value = 1; pan.positionX.setValueAtTime(panPos[0], audioContext.currentTime); @@ -252,9 +264,13 @@ const Voice: React.FC = function () { audio.setSinkId(settings.speaker); const context = new AudioContext(); - const source = context.createMediaStreamSource(stream); - const gain = context.createGain(); - const pan = context.createPanner(); + var source = context.createMediaStreamSource(stream); + let gain = context.createGain(); + let pan = context.createPanner(); + let muffle = context.createBiquadFilter(); + muffle.type = 'lowpass'; + + // let compressor = context.createDynamicsCompressor(); pan.refDistance = 0.1; pan.panningModel = 'equalpower'; pan.distanceModel = 'linear'; @@ -262,8 +278,10 @@ const Voice: React.FC = function () { pan.rolloffFactor = 1; source.connect(pan); - pan.connect(gain); - // Source -> pan -> gain -> VAD -> destination + pan.connect(muffle); + muffle.connect(gain); + + // Source -> pan -> muffle -> gain -> VAD -> destination VAD(context, gain, context.destination, { onVoiceStart: () => setTalking(true), onVoiceStop: () => setTalking(false), @@ -343,7 +361,7 @@ const Voice: React.FC = function () { for (const player of otherPlayers) { const audio = audioElements.current[playerSocketIds[player.id]]; if (audio) { - calculateVoiceAudio(gameState, settingsRef.current, myPlayer, player, audio.gain, audio.pan); + calculateVoiceAudio(gameState, settingsRef.current, myPlayer!, player, audio.gain, audio.pan, audio.muffle); if (connectionStuff.current.deafened) { audio.gain.gain.value = 0; } diff --git a/yarn.lock b/yarn.lock index 3193fa5a..e285f138 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1534,7 +1534,11 @@ ajv@^6.1.0, ajv@^6.10.1, ajv@^6.10.2, ajv@^6.12.0: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +<<<<<<< HEAD ajv@^6.10.0, ajv@^6.12.2, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.6, ajv@^6.9.1: +======= +ajv@^6.12.2, ajv@^6.12.3, ajv@^6.12.6, ajv@^6.9.1: +>>>>>>> Basic muffling (needs slight value adjustments) version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== From 85c5b85edf533692613d4ab6c917be1adef73c9e Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Sat, 12 Dec 2020 16:23:25 -0800 Subject: [PATCH 16/88] refactor: remove usage of remote and clean up IPC messaging --- src/common/ipc-messages.ts | 25 ++++++++++ src/main/GameReader.ts | 13 +++--- src/main/hook.ts | 93 ++++++++++++++------------------------ src/main/index.ts | 11 +++-- src/main/ipc-handlers.ts | 57 +++++++++++++++++++++++ src/renderer/App.tsx | 45 ++++++++++-------- src/renderer/Menu.tsx | 7 +-- src/renderer/Voice.tsx | 12 +++-- src/renderer/index.ts | 3 -- yarn.lock | 5 -- 10 files changed, 169 insertions(+), 102 deletions(-) create mode 100644 src/common/ipc-messages.ts create mode 100644 src/main/ipc-handlers.ts diff --git a/src/common/ipc-messages.ts b/src/common/ipc-messages.ts new file mode 100644 index 00000000..d6a2fc6c --- /dev/null +++ b/src/common/ipc-messages.ts @@ -0,0 +1,25 @@ +// Renderer --> Main (send/on) +export enum IpcMessages { + SHOW_ERROR_DIALOG = 'SHOW_ERROR_DIALOG', + OPEN_AMONG_US_GAME = 'OPEN_AMONG_US_GAME', + RESTART_CREWLINK = 'RESTART_CREWLINK', + QUIT_CREWLINK = 'QUIT_CREWLINK', +}; + +// Renderer --> Main (sendSync/on) +export enum IpcSyncMessages { + GET_INITIAL_STATE = 'GET_INITIAL_STATE', +} + +// Renderer --> Main (invoke/handle) +export enum IpcHandlerMessages { + START_HOOK = 'START_HOOK', +} + +// Main --> Renderer (send/on) +export enum IpcRendererMessages { + NOTIFY_GAME_OPENED = 'NOTIFY_GAME_OPENED', + NOTIFY_GAME_STATE_CHANGED = 'NOTIFY_GAME_STATE_CHANGED', + TOGGLE_DEAFEN = 'TOGGLE_DEAFEN', + PUSH_TO_TALK = 'PUSH_TO_TALK', +} diff --git a/src/main/GameReader.ts b/src/main/GameReader.ts index c9e30145..c7e9bbda 100644 --- a/src/main/GameReader.ts +++ b/src/main/GameReader.ts @@ -1,5 +1,6 @@ import { DataType, findModule, getProcesses, ModuleObject, openProcess, ProcessObject, readBuffer, readMemory as readMemoryRaw } from 'memoryjs'; import Struct from 'structron'; +import { IpcRendererMessages } from "../common/ipc-messages"; import patcher from '../patcher'; import { GameState, AmongUsState, Player } from '../common/AmongUsState'; import { IOffsets } from './IOffsets'; @@ -25,7 +26,7 @@ interface PlayerReport { } export default class GameReader { - reply: (event: string, ...args: unknown[]) => void; + sendIPC: Electron.WebContents['send']; offsets: IOffsets; PlayerStruct: Struct; @@ -47,13 +48,13 @@ export default class GameReader { try { this.amongUs = openProcess('Among Us.exe'); this.gameAssembly = findModule('GameAssembly.dll', this.amongUs.th32ProcessID); - this.reply('gameOpen', true); + this.sendIPC(IpcRendererMessages.NOTIFY_GAME_OPENED, true); } catch (e) { this.amongUs = null; } } else if (this.amongUs && !processOpen) { this.amongUs = null; - this.reply('gameOpen', false); + this.sendIPC(IpcRendererMessages.NOTIFY_GAME_OPENED, false); } return; } @@ -158,7 +159,7 @@ export default class GameReader { const patch = patcher.diff(this.lastState, newState); if (patch) { try { - this.reply('gameState', newState); + this.sendIPC(IpcRendererMessages.NOTIFY_GAME_STATE_CHANGED, newState); } catch (e) { process.exit(0); } @@ -168,8 +169,8 @@ export default class GameReader { } } - constructor(reply: (event: string, ...args: unknown[]) => void, offsets: IOffsets) { - this.reply = reply; + constructor(sendIPC: Electron.WebContents['send'], offsets: IOffsets) { + this.sendIPC = sendIPC; this.offsets = offsets; diff --git a/src/main/hook.ts b/src/main/hook.ts index 7c40ba13..03e9697e 100644 --- a/src/main/hook.ts +++ b/src/main/hook.ts @@ -1,9 +1,7 @@ -import { app, dialog, ipcMain } from 'electron'; -import path, { resolve } from 'path'; +import { ipcMain } from 'electron'; +import { resolve } from 'path'; import yml from 'js-yaml'; // import * as Struct from 'structron'; -import { HKEY, enumerateValues } from 'registry-js'; -import spawn from 'cross-spawn'; import GameReader from './GameReader'; import iohook from 'iohook'; import Store from 'electron-store'; @@ -14,6 +12,7 @@ import { createCheckers } from 'ts-interface-checker'; import TI from './hook-ti'; import { existsSync, readFileSync } from 'fs'; import { IOffsets } from './IOffsets'; +import { IpcHandlerMessages, IpcRendererMessages, IpcSyncMessages } from '../common/ipc-messages'; const { IOffsets } = createCheckers(TI); interface IOHookEvent { @@ -29,9 +28,9 @@ interface IOHookEvent { const store = new Store(); -async function loadOffsets(event: Electron.IpcMainEvent): Promise { - +async function loadOffsets(): Promise<{ success: true; offsets: IOffsets } | { success: false; error: string }> { const valuesFile = resolve((process.env.LOCALAPPDATA || '') + 'Low', 'Innersloth/Among Us/Unity/6b8b0d91-4a20-4a00-a3e4-4da4a883a5f0/Analytics/values'); + let version = ''; if (existsSync(valuesFile)) { try { @@ -39,12 +38,10 @@ async function loadOffsets(event: Electron.IpcMainEvent): Promise { - const offsets = await loadOffsets(event); - if (!readingGame && offsets) { +ipcMain.on(IpcSyncMessages.GET_INITIAL_STATE, (event) => { + if (!readingGame) { + console.error('Recieved GET_INITIAL_STATE message before the START_HOOK message was received'); + event.returnValue = null; + } + event.returnValue = gameReader.lastState; +}); + +/** + * null indicates success, failures should return an error string + */ +ipcMain.handle(IpcHandlerMessages.START_HOOK, async (event): Promise<{ error: string } | null> => { + const offsetsResults = await loadOffsets(); + if (!offsetsResults.success) { + return { error: offsetsResults.error }; + } + if (!readingGame) { readingGame = true; // Register key events iohook.on('keydown', (ev: IOHookEvent) => { const shortcutKey = store.get('pushToTalkShortcut'); if (keyCodeMatches(shortcutKey as K, ev)) { - event.reply('pushToTalk', true); + event.sender.send(IpcRendererMessages.PUSH_TO_TALK, true); } }); iohook.on('keyup', (ev: IOHookEvent) => { const shortcutKey = store.get('pushToTalkShortcut'); if (keyCodeMatches(shortcutKey as K, ev)) { - event.reply('pushToTalk', false); + event.sender.send(IpcRendererMessages.PUSH_TO_TALK, false); } if (keyCodeMatches(store.get('deafenShortcut') as K, ev)) { - event.reply('toggleDeafen'); + event.sender.send(IpcRendererMessages.TOGGLE_DEAFEN); } }); iohook.start(); // Read game memory - gameReader = new GameReader(event.reply as (event: string, ...args: unknown[]) => void, offsets); + gameReader = new GameReader(event.sender.send.bind(event.sender), offsetsResults.offsets); - ipcMain.on('initState', (event: Electron.IpcMainEvent) => { - event.returnValue = gameReader.lastState; - }); const frame = () => { gameReader.loop(); setTimeout(frame, 1000 / 20); @@ -139,7 +144,7 @@ ipcMain.on('start', async (event) => { } else if (gameReader) { gameReader.amongUs = null; } - event.reply('started'); + return null; }); const keycodeMap = { @@ -169,33 +174,3 @@ function keyCodeMatches(key: K, ev: IOHookEvent): boolean { return false; } } - - - -ipcMain.on('openGame', () => { - // Get steam path from registry - const steamPath = enumerateValues(HKEY.HKEY_LOCAL_MACHINE, - 'SOFTWARE\\WOW6432Node\\Valve\\Steam') - .find(v => v.name === 'InstallPath'); - // Check if Steam is installed - if (!steamPath) { - dialog.showErrorBox('Error', 'Could not find your Steam install path.'); - } else { - try { - const process = spawn(path.join(steamPath.data as string, 'steam.exe'), [ - '-applaunch', - '945360' - ]); - process.on('error', () => { - dialog.showErrorBox('Error', 'Please launch the game through Steam.'); - }); - } catch (e) { - dialog.showErrorBox('Error', 'Please launch the game through Steam.'); - } - } -}); - -ipcMain.on('relaunch', () => { - app.relaunch(); - app.quit(); -}); diff --git a/src/main/index.ts b/src/main/index.ts index b271c0f6..f1a6ac11 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -6,6 +6,7 @@ import windowStateKeeper from 'electron-window-state'; import { join as joinPath } from 'path'; import { format as formatUrl } from 'url'; import './hook'; +import { initializeIpcHandlers, initializeIpcListeners } from './ipc-handlers'; const isDevelopment = process.env.NODE_ENV !== 'production'; @@ -34,7 +35,6 @@ function createMainWindow() { transparent: true, webPreferences: { nodeIntegration: true, - enableRemoteModule: true, webSecurity: false } }); @@ -42,7 +42,10 @@ function createMainWindow() { mainWindowState.manage(window); if (isDevelopment) { - window.webContents.openDevTools(); + // Force devtools into detached mode otherwise they are unusable + window.webContents.openDevTools({ + mode: 'detach' + }); } if (isDevelopment) { @@ -103,7 +106,9 @@ if (!gotTheLock) { }); // create main BrowserWindow when electron is ready - app.on('ready', () => { + app.whenReady().then(() => { + initializeIpcListeners(); + initializeIpcHandlers(); mainWindow = createMainWindow(); }); } \ No newline at end of file diff --git a/src/main/ipc-handlers.ts b/src/main/ipc-handlers.ts new file mode 100644 index 00000000..d470c7f2 --- /dev/null +++ b/src/main/ipc-handlers.ts @@ -0,0 +1,57 @@ +import { app, BrowserWindow, dialog, ipcMain } from 'electron'; +import { HKEY, enumerateValues } from 'registry-js'; +import spawn from 'cross-spawn'; +import path from 'path'; + +import { IpcMessages } from '../common/ipc-messages'; + +// Listeners are fire and forget, they do not have "responses" or return values +export const initializeIpcListeners = () => { + ipcMain.on(IpcMessages.SHOW_ERROR_DIALOG, (e, opts: { title: string; content: string; }) => { + if (typeof opts === 'object' && opts && typeof opts.title === 'string' && typeof opts.content === 'string') { + dialog.showErrorBox(opts.title, opts.content); + } + }); + + ipcMain.on(IpcMessages.OPEN_AMONG_US_GAME, () => { + // Get steam path from registry + const steamPath = enumerateValues(HKEY.HKEY_LOCAL_MACHINE, + 'SOFTWARE\\WOW6432Node\\Valve\\Steam') + .find(v => v.name === 'InstallPath'); + // Check if Steam is installed + if (!steamPath) { + dialog.showErrorBox('Error', 'Could not find your Steam install path.'); + } else { + try { + const process = spawn(path.join(steamPath.data as string, 'steam.exe'), [ + '-applaunch', + '945360' + ]); + process.on('error', () => { + dialog.showErrorBox('Error', 'Please launch the game through Steam.'); + }); + } catch (e) { + dialog.showErrorBox('Error', 'Please launch the game through Steam.'); + } + } + }) + + ipcMain.on(IpcMessages.RESTART_CREWLINK, () => { + app.relaunch(); + app.quit(); + }); + + ipcMain.on(IpcMessages.QUIT_CREWLINK, () => { + for (const win of BrowserWindow.getAllWindows()) { + win.close(); + } + app.quit(); + }); +} + +// Handlers are async cross-process instructions, they should have a return value +// or the caller should be "await"'ing them. If neither of these are the case +// consider making it a "listener" instead for performance and readability +export const initializeIpcHandlers = () => { + // TODO: Put handlers here +} \ No newline at end of file diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index c242cdd0..7b685e3f 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -2,10 +2,11 @@ import React, { useEffect, useReducer, useState } from 'react'; import ReactDOM from 'react-dom'; import Voice from './Voice'; import Menu from './Menu'; -import { ipcRenderer, remote } from 'electron'; +import { ipcRenderer } from 'electron'; import { AmongUsState } from '../common/AmongUsState'; import Settings, { settingsReducer } from './Settings'; import { GameStateContext, SettingsContext } from './contexts'; +import { IpcHandlerMessages, IpcMessages, IpcRendererMessages, IpcSyncMessages } from '../common/ipc-messages'; let appVersion = ''; if (typeof window !== 'undefined' && window.location) { @@ -45,23 +46,31 @@ function App() { setGameState(newState); }; let shouldInit = true; - const onError = (_: Electron.IpcRendererEvent, error: string) => { - alert(error + '\n\nRestart the app after you fix this.'); - shouldInit = false; - setErrored(true); - }; - ipcRenderer.on('gameOpen', onOpen); - ipcRenderer.on('error', onError); - ipcRenderer.on('gameState', onState); - ipcRenderer.once('started', () => { - if (shouldInit) - setGameState(ipcRenderer.sendSync('initState')); + ipcRenderer.invoke(IpcHandlerMessages.START_HOOK).then((error: { error: string } | null) => { + console.log({ result: error }) + if (shouldInit) { + if (error) { + alert(error.error + '\n\nRestart the app after you fix this.'); + shouldInit = false; + setErrored(true); + } else { + setGameState(ipcRenderer.sendSync(IpcSyncMessages.GET_INITIAL_STATE)); + } + } + }).catch((error: Error) => { + if (shouldInit) { + alert(error + '\n\nRestart the app after you fix this.'); + shouldInit = false; + setErrored(true); + } }); + ipcRenderer.on(IpcRendererMessages.NOTIFY_GAME_OPENED, onOpen); + ipcRenderer.on(IpcRendererMessages.NOTIFY_GAME_STATE_CHANGED, onState); return () => { - ipcRenderer.off('gameOpen', onOpen); - ipcRenderer.off('error', onError); - ipcRenderer.off('gameState', onState); - }; + ipcRenderer.off(IpcRendererMessages.NOTIFY_GAME_OPENED, onOpen); + ipcRenderer.off(IpcRendererMessages.NOTIFY_GAME_STATE_CHANGED, onState); + shouldInit = false; + } }, []); @@ -85,9 +94,7 @@ function App() { - { - remote.getCurrentWindow().close(); - }}> + ipcRenderer.send(IpcMessages.QUIT_CREWLINK)}> diff --git a/src/renderer/Menu.tsx b/src/renderer/Menu.tsx index f96791e1..54edc107 100644 --- a/src/renderer/Menu.tsx +++ b/src/renderer/Menu.tsx @@ -2,7 +2,8 @@ import React from 'react'; import { ImpulseSpinner as Spinner } from 'react-spinners-kit'; import { ipcRenderer } from 'electron'; import './css/menu.css'; -import Footer from './Footer'; +import Footer from "./Footer"; +import { IpcMessages } from "../common/ipc-messages"; export interface MenuProps { errored: boolean @@ -23,7 +24,7 @@ const Menu: React.FC = function ({ errored }: MenuProps) { : @@ -31,7 +32,7 @@ const Menu: React.FC = function ({ errored }: MenuProps) { Waiting for Among Us } diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index ebaf4e08..8d71d5c3 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -4,9 +4,10 @@ import Avatar from './Avatar'; import { GameStateContext, SettingsContext } from './contexts'; import { AmongUsState, GameState, Player } from '../common/AmongUsState'; import Peer from 'simple-peer'; -import { ipcRenderer, remote } from 'electron'; +import { ipcRenderer } from 'electron'; import VAD from './vad'; import { ISettings } from '../common/ISettings'; +import { IpcMessages, IpcRendererMessages } from '../common/ipc-messages'; export interface ExtendedAudioElement extends HTMLAudioElement { setSinkId: (sinkId: string) => Promise; @@ -180,12 +181,12 @@ const Voice: React.FC = function () { stream.getAudioTracks()[0].enabled = !settings.pushToTalk; - ipcRenderer.on('toggleDeafen', () => { + ipcRenderer.on(IpcRendererMessages.TOGGLE_DEAFEN, () => { connectionStuff.current.deafened = !connectionStuff.current.deafened; stream.getAudioTracks()[0].enabled = !connectionStuff.current.deafened; setDeafened(connectionStuff.current.deafened); }); - ipcRenderer.on('pushToTalk', (_: unknown, pressing: boolean) => { + ipcRenderer.on(IpcRendererMessages.PUSH_TO_TALK, (_: unknown, pressing: boolean) => { if (!connectionStuff.current.pushToTalk) return; if (!connectionStuff.current.deafened) { stream.getAudioTracks()[0].enabled = pressing; @@ -311,7 +312,10 @@ const Voice: React.FC = function () { }, (error) => { console.error(error); - remote.dialog.showErrorBox('Error', 'Couldn\'t connect to your microphone:\n' + error); + ipcRenderer.send(IpcMessages.SHOW_ERROR_DIALOG, { + title: 'Error', + content: 'Couldn\'t connect to your microphone:\n' + error + }); }); return () => { diff --git a/src/renderer/index.ts b/src/renderer/index.ts index 32b03a24..34f6228b 100644 --- a/src/renderer/index.ts +++ b/src/renderer/index.ts @@ -1,5 +1,2 @@ -import { ipcRenderer } from 'electron'; import './App'; import './css/index.css'; - -ipcRenderer.send('start'); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 3193fa5a..57ab37f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1844,11 +1844,6 @@ atomically@^1.3.1: resolved "https://registry.yarnpkg.com/atomically/-/atomically-1.6.0.tgz#d8d47f99834dbb88bd6266cc69a1447e2f3675ec" integrity sha512-mu394MH+yY2TSKMyH+978PcGMZ8sRNks2PuVeH6c2ED4mimR2LEE039MVcIGVhtmG54cKEMh4gKhxKL/CLaX/w== -audio-activity@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/audio-activity/-/audio-activity-1.0.0.tgz#f653b9668582e7fd6a4c9b9997abe26607bcc456" - integrity sha1-9lO5ZoWC5/1qTJuZl6viZge8xFY= - audio-frequency-to-index@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/audio-frequency-to-index/-/audio-frequency-to-index-2.0.0.tgz#4c4bca9f3bfec38c773aa6b5604c117adc982d45" From 659f537fdab7f7316738fd6f0cefcf540bbd4681 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Sat, 12 Dec 2020 16:33:46 -0800 Subject: [PATCH 17/88] chore: run linter --- src/common/ipc-messages.ts | 2 +- src/main/GameReader.ts | 2 +- src/main/ipc-handlers.ts | 80 +++++++++++++++++++------------------- src/renderer/App.tsx | 3 +- src/renderer/Menu.tsx | 4 +- 5 files changed, 45 insertions(+), 46 deletions(-) diff --git a/src/common/ipc-messages.ts b/src/common/ipc-messages.ts index d6a2fc6c..7bd6cb9f 100644 --- a/src/common/ipc-messages.ts +++ b/src/common/ipc-messages.ts @@ -4,7 +4,7 @@ export enum IpcMessages { OPEN_AMONG_US_GAME = 'OPEN_AMONG_US_GAME', RESTART_CREWLINK = 'RESTART_CREWLINK', QUIT_CREWLINK = 'QUIT_CREWLINK', -}; +} // Renderer --> Main (sendSync/on) export enum IpcSyncMessages { diff --git a/src/main/GameReader.ts b/src/main/GameReader.ts index c7e9bbda..44280664 100644 --- a/src/main/GameReader.ts +++ b/src/main/GameReader.ts @@ -1,6 +1,6 @@ import { DataType, findModule, getProcesses, ModuleObject, openProcess, ProcessObject, readBuffer, readMemory as readMemoryRaw } from 'memoryjs'; import Struct from 'structron'; -import { IpcRendererMessages } from "../common/ipc-messages"; +import { IpcRendererMessages } from '../common/ipc-messages'; import patcher from '../patcher'; import { GameState, AmongUsState, Player } from '../common/AmongUsState'; import { IOffsets } from './IOffsets'; diff --git a/src/main/ipc-handlers.ts b/src/main/ipc-handlers.ts index d470c7f2..ce8a7edb 100644 --- a/src/main/ipc-handlers.ts +++ b/src/main/ipc-handlers.ts @@ -7,51 +7,51 @@ import { IpcMessages } from '../common/ipc-messages'; // Listeners are fire and forget, they do not have "responses" or return values export const initializeIpcListeners = () => { - ipcMain.on(IpcMessages.SHOW_ERROR_DIALOG, (e, opts: { title: string; content: string; }) => { - if (typeof opts === 'object' && opts && typeof opts.title === 'string' && typeof opts.content === 'string') { - dialog.showErrorBox(opts.title, opts.content); - } - }); + ipcMain.on(IpcMessages.SHOW_ERROR_DIALOG, (e, opts: { title: string; content: string; }) => { + if (typeof opts === 'object' && opts && typeof opts.title === 'string' && typeof opts.content === 'string') { + dialog.showErrorBox(opts.title, opts.content); + } + }); - ipcMain.on(IpcMessages.OPEN_AMONG_US_GAME, () => { - // Get steam path from registry - const steamPath = enumerateValues(HKEY.HKEY_LOCAL_MACHINE, - 'SOFTWARE\\WOW6432Node\\Valve\\Steam') - .find(v => v.name === 'InstallPath'); - // Check if Steam is installed - if (!steamPath) { - dialog.showErrorBox('Error', 'Could not find your Steam install path.'); - } else { - try { - const process = spawn(path.join(steamPath.data as string, 'steam.exe'), [ - '-applaunch', - '945360' - ]); - process.on('error', () => { - dialog.showErrorBox('Error', 'Please launch the game through Steam.'); - }); - } catch (e) { - dialog.showErrorBox('Error', 'Please launch the game through Steam.'); - } - } - }) + ipcMain.on(IpcMessages.OPEN_AMONG_US_GAME, () => { + // Get steam path from registry + const steamPath = enumerateValues(HKEY.HKEY_LOCAL_MACHINE, + 'SOFTWARE\\WOW6432Node\\Valve\\Steam') + .find(v => v.name === 'InstallPath'); + // Check if Steam is installed + if (!steamPath) { + dialog.showErrorBox('Error', 'Could not find your Steam install path.'); + } else { + try { + const process = spawn(path.join(steamPath.data as string, 'steam.exe'), [ + '-applaunch', + '945360' + ]); + process.on('error', () => { + dialog.showErrorBox('Error', 'Please launch the game through Steam.'); + }); + } catch (e) { + dialog.showErrorBox('Error', 'Please launch the game through Steam.'); + } + } + }); - ipcMain.on(IpcMessages.RESTART_CREWLINK, () => { - app.relaunch(); - app.quit(); - }); + ipcMain.on(IpcMessages.RESTART_CREWLINK, () => { + app.relaunch(); + app.quit(); + }); - ipcMain.on(IpcMessages.QUIT_CREWLINK, () => { - for (const win of BrowserWindow.getAllWindows()) { - win.close(); - } - app.quit(); - }); -} + ipcMain.on(IpcMessages.QUIT_CREWLINK, () => { + for (const win of BrowserWindow.getAllWindows()) { + win.close(); + } + app.quit(); + }); +}; // Handlers are async cross-process instructions, they should have a return value // or the caller should be "await"'ing them. If neither of these are the case // consider making it a "listener" instead for performance and readability export const initializeIpcHandlers = () => { - // TODO: Put handlers here -} \ No newline at end of file + // TODO: Put handlers here +}; \ No newline at end of file diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 7b685e3f..9863618a 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -47,7 +47,6 @@ function App() { }; let shouldInit = true; ipcRenderer.invoke(IpcHandlerMessages.START_HOOK).then((error: { error: string } | null) => { - console.log({ result: error }) if (shouldInit) { if (error) { alert(error.error + '\n\nRestart the app after you fix this.'); @@ -70,7 +69,7 @@ function App() { ipcRenderer.off(IpcRendererMessages.NOTIFY_GAME_OPENED, onOpen); ipcRenderer.off(IpcRendererMessages.NOTIFY_GAME_STATE_CHANGED, onState); shouldInit = false; - } + }; }, []); diff --git a/src/renderer/Menu.tsx b/src/renderer/Menu.tsx index 54edc107..6298fb2a 100644 --- a/src/renderer/Menu.tsx +++ b/src/renderer/Menu.tsx @@ -2,8 +2,8 @@ import React from 'react'; import { ImpulseSpinner as Spinner } from 'react-spinners-kit'; import { ipcRenderer } from 'electron'; import './css/menu.css'; -import Footer from "./Footer"; -import { IpcMessages } from "../common/ipc-messages"; +import Footer from './Footer'; +import { IpcMessages } from '../common/ipc-messages'; export interface MenuProps { errored: boolean From 7efcc9398952e40de8cd2bc93a75621aa618cad6 Mon Sep 17 00:00:00 2001 From: Brian Luo Date: Sat, 12 Dec 2020 17:40:42 -0800 Subject: [PATCH 18/88] Reduce audio volume during muffle --- src/renderer/Voice.tsx | 32 +++++++++++++++++--------------- yarn.lock | 9 --------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index 8e4307a3..12bada9c 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -73,16 +73,6 @@ function calculateVoiceAudio(state: AmongUsState, settings: ISettings, me: Playe return; } - if (me.inVent) { - // Enable muffle - muffle.frequency.value = 400; - muffle.Q.value = 20; - } else { - // Disable muffle - muffle.frequency.value = 20000; - muffle.Q.value = 0; - } - if (state.gameState === GameState.LOBBY || state.gameState === GameState.DISCUSSION) { gain.gain.value = 1; pan.positionX.setValueAtTime(panPos[0], audioContext.currentTime); @@ -97,6 +87,18 @@ function calculateVoiceAudio(state: AmongUsState, settings: ISettings, me: Playe if (gain.gain.value === 1 && Math.sqrt(Math.pow(panPos[0], 2) + Math.pow(panPos[1], 2)) > 7) { gain.gain.value = 0; } + + if (me.inVent) { + // Enable muffle + muffle.frequency.value = 400; + muffle.Q.value = 20; + if (gain.gain.value === 1) + gain.gain.value = 0.7; // Too loud at 1 + } else { + // Disable muffle + muffle.frequency.value = 20000; + muffle.Q.value = 0; + } } @@ -264,10 +266,10 @@ const Voice: React.FC = function () { audio.setSinkId(settings.speaker); const context = new AudioContext(); - var source = context.createMediaStreamSource(stream); - let gain = context.createGain(); - let pan = context.createPanner(); - let muffle = context.createBiquadFilter(); + const source = context.createMediaStreamSource(stream); + const gain = context.createGain(); + const pan = context.createPanner(); + const muffle = context.createBiquadFilter(); muffle.type = 'lowpass'; // let compressor = context.createDynamicsCompressor(); @@ -297,7 +299,7 @@ const Voice: React.FC = function () { return socketPlayerIds; }); }; - audioElements.current[peer] = { element: audio, gain, pan }; + audioElements.current[peer] = { element: audio, gain, pan, muffle }; }); connection.on('signal', (data) => { socket.emit('signal', { diff --git a/yarn.lock b/yarn.lock index e285f138..57ab37f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1534,11 +1534,7 @@ ajv@^6.1.0, ajv@^6.10.1, ajv@^6.10.2, ajv@^6.12.0: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -<<<<<<< HEAD ajv@^6.10.0, ajv@^6.12.2, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.6, ajv@^6.9.1: -======= -ajv@^6.12.2, ajv@^6.12.3, ajv@^6.12.6, ajv@^6.9.1: ->>>>>>> Basic muffling (needs slight value adjustments) version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -1848,11 +1844,6 @@ atomically@^1.3.1: resolved "https://registry.yarnpkg.com/atomically/-/atomically-1.6.0.tgz#d8d47f99834dbb88bd6266cc69a1447e2f3675ec" integrity sha512-mu394MH+yY2TSKMyH+978PcGMZ8sRNks2PuVeH6c2ED4mimR2LEE039MVcIGVhtmG54cKEMh4gKhxKL/CLaX/w== -audio-activity@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/audio-activity/-/audio-activity-1.0.0.tgz#f653b9668582e7fd6a4c9b9997abe26607bcc456" - integrity sha1-9lO5ZoWC5/1qTJuZl6viZge8xFY= - audio-frequency-to-index@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/audio-frequency-to-index/-/audio-frequency-to-index-2.0.0.tgz#4c4bca9f3bfec38c773aa6b5604c117adc982d45" From ccf86765f8472c0f273c085e333520927b7299bb Mon Sep 17 00:00:00 2001 From: Ryan Shavell Date: Sat, 12 Dec 2020 22:54:03 -0500 Subject: [PATCH 19/88] Overlay position control --- src/common/ISettings.d.ts | 1 + src/renderer/App.tsx | 3 ++- src/renderer/Overlay.tsx | 31 +++++++++++++++++-------------- src/renderer/Settings.tsx | 30 +++++++++++++++++++++++------- 4 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/common/ISettings.d.ts b/src/common/ISettings.d.ts index 60401a83..3a53535b 100644 --- a/src/common/ISettings.d.ts +++ b/src/common/ISettings.d.ts @@ -14,4 +14,5 @@ export interface ISettings { hideCode: boolean; enableSpatialAudio: boolean; compactOverlay: boolean; + overlayPosition: string; } diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 5e1ac1a6..1b23a248 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -34,7 +34,8 @@ export default function App() { }, hideCode: false, enableSpatialAudio: true, - compactOverlay: false + compactOverlay: false, + overlayPosition: 'top' }); diff --git a/src/renderer/Overlay.tsx b/src/renderer/Overlay.tsx index e7555ee7..7647cccb 100644 --- a/src/renderer/Overlay.tsx +++ b/src/renderer/Overlay.tsx @@ -28,15 +28,6 @@ export default function Overlay() { if (!gameState || !gameState.players) return undefined; else return gameState.players.find(p => p.isLocal); }, [gameState]); - - /*const otherPlayers = useMemo(() => { - let otherPlayers: Player[]; - if (!gameState || !gameState.players || gameState.lobbyCode === 'MENU' || !myPlayer) otherPlayers = []; - else otherPlayers = gameState.players.filter(p => !p.isLocal); - - return otherPlayers; - }, [gameState]);*/ - const relevantPlayers = useMemo(() => { let relevantPlayers: Player[]; @@ -123,6 +114,7 @@ export default function Overlay() { document.body.style.backgroundColor = "rgba(255, 255, 255, 0)"; document.body.style.paddingTop = "0"; + var baseCSS:any = { backgroundColor: "rgba(0, 0, 0, 0.85)", width: "100px", @@ -131,6 +123,7 @@ export default function Overlay() { marginTop: "-16px", paddingLeft: "8px", }; + var playersCSS:any = {} var topArea =

CrewLink ({status})

var playerList:Player[] = []; if (gameState.players && gameState.gameState != GameState.MENU) playerList = relevantPlayers;//gameState.players; @@ -139,13 +132,23 @@ export default function Overlay() { baseCSS["left"] = "8px"; baseCSS["top"] = "60px"; } else { - baseCSS["marginLeft"] = "auto"; - baseCSS["marginRight"] = "auto"; - baseCSS["marginTop"] = "0px"; baseCSS["paddingTop"] = "8px"; baseCSS["paddingLeft"] = "0px"; baseCSS["width"] = "800px"; - baseCSS["backgroundColor"] = "rgba(0, 0, 0, 0.5)"; //0.25 + baseCSS["backgroundColor"] = "rgba(0, 0, 0, 0.5)"; + if (settings.overlayPosition == 'top') { + baseCSS["marginLeft"] = "auto"; + baseCSS["marginRight"] = "auto"; + baseCSS["marginTop"] = "0px"; + } else if (settings.overlayPosition == 'bottom_left') { + baseCSS["position"] = "absolute"; + baseCSS["bottom"] = "0px"; + baseCSS["backgroundColor"] = "rgba(0, 0, 0, 0.35)"; + baseCSS["width"] = null; + + playersCSS["justifyContent"] = "left" + playersCSS["alignItems"] = "left" + } topArea = <>; if ((settings.compactOverlay || gameState.gameState == GameState.TASKS) && playerList) { playerList = talkingPlayers; @@ -155,7 +158,7 @@ export default function Overlay() { var playerArea:JSX.Element = <>; if (playerList) { - playerArea =
+ playerArea =
{ playerList.map(player => { const connected = Object.values(socketPlayerIds).includes(player.id) || player.isLocal; diff --git a/src/renderer/Settings.tsx b/src/renderer/Settings.tsx index 4faf0ccb..a980097e 100644 --- a/src/renderer/Settings.tsx +++ b/src/renderer/Settings.tsx @@ -103,7 +103,11 @@ const store = new Store({ compactOverlay: { type: 'boolean', default: false - } + }, + overlayPosition: { + type: 'string', + default: 'top' + }, } }); @@ -321,18 +325,30 @@ const Settings: React.FC = function ({ open, onClose }: SettingsP
-
- - Exit to apply changes - +
+ +
-
setSettings({ +
setSettings({ type: 'setOne', action: ['compactOverlay', !settings.compactOverlay] })}> - +
+
+ + Exit to apply changes + +
; }; From 4b6df0ce7f07dbbf8f0e87e6af130c88c9b8fcac Mon Sep 17 00:00:00 2001 From: Ryan Shavell Date: Sat, 12 Dec 2020 23:03:47 -0500 Subject: [PATCH 20/88] Formatting --- src/renderer/Overlay.tsx | 56 ++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/renderer/Overlay.tsx b/src/renderer/Overlay.tsx index 7647cccb..ce33b91e 100644 --- a/src/renderer/Overlay.tsx +++ b/src/renderer/Overlay.tsx @@ -116,17 +116,17 @@ export default function Overlay() { document.body.style.paddingTop = "0"; var baseCSS:any = { - backgroundColor: "rgba(0, 0, 0, 0.85)", - width: "100px", - borderRadius: "8px", - position: "relative", - marginTop: "-16px", - paddingLeft: "8px", - }; - var playersCSS:any = {} + backgroundColor: "rgba(0, 0, 0, 0.85)", + width: "100px", + borderRadius: "8px", + position: "relative", + marginTop: "-16px", + paddingLeft: "8px", + }; var topArea =

CrewLink ({status})

+ var playersCSS:any = {} var playerList:Player[] = []; - if (gameState.players && gameState.gameState != GameState.MENU) playerList = relevantPlayers;//gameState.players; + if (gameState.players && gameState.gameState != GameState.MENU) playerList = relevantPlayers; if (gameState.gameState == GameState.UNKNOWN || gameState.gameState == GameState.MENU) { baseCSS["left"] = "8px"; @@ -158,26 +158,26 @@ export default function Overlay() { var playerArea:JSX.Element = <>; if (playerList) { - playerArea =
- { - playerList.map(player => { - const connected = Object.values(socketPlayerIds).includes(player.id) || player.isLocal; - let name = settings.compactOverlay ? "" : {player.name} - return ( -
-
- -
- {name} + playerArea =
+ { + playerList.map(player => { + const connected = Object.values(socketPlayerIds).includes(player.id) || player.isLocal; + let name = settings.compactOverlay ? "" : {player.name} + return ( +
+
+
- ); - }) - } -
+ {name} +
+ ); + }) + } +
} From aee7e0b1f88e5f648ed802449185363c6b7d5865 Mon Sep 17 00:00:00 2001 From: Ryan Shavell Date: Sat, 12 Dec 2020 23:26:39 -0500 Subject: [PATCH 21/88] Remove debug comments --- src/main/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index e48ed0a1..378da4b4 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -118,8 +118,6 @@ if (!gotTheLock) { slashes: true })) } - //overlay.webContents.openDevTools() - //overlayWindow.attachTo(overlay, 'Untitled - Notepad') overlay.setIgnoreMouseEvents(true); overlayWindow.attachTo(overlay, 'Among Us') From e59122eac29b1c9f7dd724c30b0c423003918e33 Mon Sep 17 00:00:00 2001 From: Ryan Shavell Date: Sat, 12 Dec 2020 23:47:07 -0500 Subject: [PATCH 22/88] Bugfixes --- src/main/index.ts | 4 ++-- src/renderer/Overlay.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index 378da4b4..a2de6d85 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -59,7 +59,8 @@ function createMainWindow() { pathname: joinPath(__dirname, 'index.html'), protocol: 'file', query: { - version: autoUpdater.currentVersion.version + version: autoUpdater.currentVersion.version, + view: "app" }, slashes: true })); @@ -107,7 +108,6 @@ if (!gotTheLock) { if (isDevelopment) { overlay.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}?version=${autoUpdater.currentVersion.version}&view=overlay`) } else { - overlay.loadURL(formatUrl({ pathname: joinPath(__dirname, 'index.html'), protocol: 'file', diff --git a/src/renderer/Overlay.tsx b/src/renderer/Overlay.tsx index ce33b91e..31ac7526 100644 --- a/src/renderer/Overlay.tsx +++ b/src/renderer/Overlay.tsx @@ -169,7 +169,7 @@ export default function Overlay() {
{name} From a91339a825fce1eda0040dfe85a1703da25bbad1 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Sun, 13 Dec 2020 14:28:37 -0800 Subject: [PATCH 23/88] fix: add missing early return --- src/main/hook.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/hook.ts b/src/main/hook.ts index 03e9697e..842ab841 100644 --- a/src/main/hook.ts +++ b/src/main/hook.ts @@ -99,6 +99,7 @@ ipcMain.on(IpcSyncMessages.GET_INITIAL_STATE, (event) => { if (!readingGame) { console.error('Recieved GET_INITIAL_STATE message before the START_HOOK message was received'); event.returnValue = null; + return; } event.returnValue = gameReader.lastState; }); From 698b1b69497edcbd5063c0a58ef0f6a673bf3933 Mon Sep 17 00:00:00 2001 From: Brian Luo Date: Mon, 14 Dec 2020 20:02:13 -0800 Subject: [PATCH 24/88] Disconnect muffle audio element --- src/renderer/Voice.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index 12bada9c..09dc8590 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -238,6 +238,7 @@ const Voice: React.FC = function () { if (audioElements.current[peer]) { document.body.removeChild(audioElements.current[peer].element); audioElements.current[peer].pan.disconnect(); + audioElements.current[peer].muffle.disconnect(); audioElements.current[peer].gain.disconnect(); delete audioElements.current[peer]; } From 2a256f0bf9b5bfb674734f80e3d5911eeb00f58e Mon Sep 17 00:00:00 2001 From: Ryan Shavell Date: Thu, 17 Dec 2020 21:04:27 -0500 Subject: [PATCH 25/88] Close & garbage-collect the overlay window to prevent any memory leaks --- src/main/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/index.ts b/src/main/index.ts index a2de6d85..c5244db6 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -68,6 +68,10 @@ function createMainWindow() { window.on('closed', () => { global.mainWindow = null; + if (global.overlay != null) { + global.overlay.close() + global.overlay = null; + } }); window.webContents.on('devtools-opened', () => { @@ -128,6 +132,10 @@ if (!gotTheLock) { app.on('window-all-closed', () => { // on macOS it is common for applications to stay open until the user explicitly quits if (process.platform !== 'darwin') { + if (global.overlay != null) { + global.overlay.close() + global.overlay = null; + } app.quit(); } }); From a9ea9a8fb26e7a412879666a0d15ea0fb38dc87f Mon Sep 17 00:00:00 2001 From: OhMyGuus Date: Tue, 22 Dec 2020 02:40:39 +0100 Subject: [PATCH 26/88] - Support for X64 version of the game (Epic , WIP) - Changed method of getting the gamecode (works in game now) - Added patternscanning so you wont need to update the server evrey time --- src/main/GameReader.ts | 187 +++++++++++++++++++++++++---------------- src/main/IOffsets.d.ts | 17 ++++ src/main/hook-ti.ts | 22 ++++- src/main/hook.ts | 39 +++------ src/main/memoryjs.d.ts | 2 + 5 files changed, 167 insertions(+), 100 deletions(-) diff --git a/src/main/GameReader.ts b/src/main/GameReader.ts index c9e30145..aa6af552 100644 --- a/src/main/GameReader.ts +++ b/src/main/GameReader.ts @@ -1,8 +1,8 @@ -import { DataType, findModule, getProcesses, ModuleObject, openProcess, ProcessObject, readBuffer, readMemory as readMemoryRaw } from 'memoryjs'; +import { DataType, findModule, getProcesses, ModuleObject, openProcess, ProcessObject, readBuffer, readMemory as readMemoryRaw, findPattern as findPatternRaw } from 'memoryjs'; import Struct from 'structron'; import patcher from '../patcher'; import { GameState, AmongUsState, Player } from '../common/AmongUsState'; -import { IOffsets } from './IOffsets'; +import { IOffsets, IOffsetsContainer } from './IOffsets'; interface ValueType { @@ -26,19 +26,21 @@ interface PlayerReport { export default class GameReader { reply: (event: string, ...args: unknown[]) => void; - offsets: IOffsets; - PlayerStruct: Struct; + offsets?: IOffsets; + offsetsContainer: IOffsetsContainer; + PlayerStruct?: Struct; menuUpdateTimer = 20; lastPlayerPtr = 0; shouldReadLobby = false; exileCausesEnd = false; + is_64bit: boolean = false; oldGameState = GameState.UNKNOWN; lastState: AmongUsState = {} as AmongUsState; - amongUs: ProcessObject | null = null; gameAssembly: ModuleObject | null = null; + gameCode = 'MENU'; checkProcessOpen(): void { @@ -47,6 +49,7 @@ export default class GameReader { try { this.amongUs = openProcess('Among Us.exe'); this.gameAssembly = findModule('GameAssembly.dll', this.amongUs.th32ProcessID); + this.initializeoffsets(); this.reply('gameOpen', true); } catch (e) { this.amongUs = null; @@ -60,34 +63,34 @@ export default class GameReader { loop(): void { this.checkProcessOpen(); - if (this.amongUs !== null && this.gameAssembly !== null) { + if (this.PlayerStruct && this.offsets && this.amongUs !== null && this.gameAssembly !== null) { let state = GameState.UNKNOWN; const meetingHud = this.readMemory('pointer', this.gameAssembly.modBaseAddr, this.offsets.meetingHud); - const meetingHud_cachePtr = meetingHud === 0 ? 0 : this.readMemory('uint32', meetingHud, this.offsets.meetingHudCachePtr); + const meetingHud_cachePtr = meetingHud === 0 ? 0 : this.readMemory('pointer', meetingHud, this.offsets.meetingHudCachePtr); const meetingHudState = meetingHud_cachePtr === 0 ? 4 : this.readMemory('int', meetingHud, this.offsets.meetingHudState, 4); const gameState = this.readMemory('int', this.gameAssembly.modBaseAddr, this.offsets.gameState); switch (gameState) { - case 0: - state = GameState.MENU; - this.exileCausesEnd = false; - break; - case 1: - case 3: - state = GameState.LOBBY; - this.exileCausesEnd = false; - break; - default: - if (this.exileCausesEnd) + case 0: + state = GameState.MENU; + this.exileCausesEnd = false; + break; + case 1: + case 3: state = GameState.LOBBY; - else if (meetingHudState < 4) - state = GameState.DISCUSSION; - else - state = GameState.TASKS; - break; + this.exileCausesEnd = false; + break; + default: + if (this.exileCausesEnd) + state = GameState.LOBBY; + else if (meetingHudState < 4) + state = GameState.DISCUSSION; + else + state = GameState.TASKS; + break; } - - const allPlayersPtr = this.readMemory('ptr', this.gameAssembly.modBaseAddr, this.offsets.allPlayersPtr) & 0xffffffff; + this.gameCode = this.IntToGameCode(this.readMemory('int32', this.gameAssembly.modBaseAddr, this.offsets.gameCode)) + const allPlayersPtr = this.readMemory('ptr', this.gameAssembly.modBaseAddr, this.offsets.allPlayersPtr); const allPlayers = this.readMemory('ptr', allPlayersPtr, this.offsets.allPlayers); const playerCount = this.readMemory('int' as const, allPlayersPtr, this.offsets.playerCount); let playerAddrPtr = allPlayers + this.offsets.playerAddrPtr; @@ -96,19 +99,22 @@ export default class GameReader { const exiledPlayerId = this.readMemory('byte', this.gameAssembly.modBaseAddr, this.offsets.exiledPlayerId); let impostors = 0, crewmates = 0; - for (let i = 0; i < Math.min(playerCount, 10); i++) { - const { address, last } = this.offsetAddress(playerAddrPtr, this.offsets.player.offsets); - const playerData = readBuffer(this.amongUs.handle, address + last, this.offsets.player.bufferLength); - const player = this.parsePlayer(address + last, playerData); - playerAddrPtr += 4; - players.push(player); + if (this.gameCode) { + for (let i = 0; i < Math.min(playerCount, 10); i++) { + const { address, last } = this.offsetAddress(playerAddrPtr, this.offsets.player.offsets); + const playerData = readBuffer(this.amongUs.handle, address + last, this.offsets.player.bufferLength); + const player = this.parsePlayer(address + last, playerData); + playerAddrPtr += 4; + if (!player) continue; + players.push(player); - if (player.name === '' || player.id === exiledPlayerId || player.isDead || player.disconnected) continue; + if (player.name === '' || player.id === exiledPlayerId || player.isDead || player.disconnected) continue; - if (player.isImpostor) - impostors++; - else - crewmates++; + if (player.isImpostor) + impostors++; + else + crewmates++; + } } if (this.oldGameState === GameState.DISCUSSION && state === GameState.TASKS) { @@ -126,31 +132,9 @@ export default class GameReader { } this.lastPlayerPtr = allPlayers; - const inGame = state === GameState.TASKS || state === GameState.DISCUSSION || state === GameState.LOBBY; - let newGameCode = 'MENU'; - if (state === GameState.LOBBY) { - newGameCode = this.readString( - this.readMemory('int32', this.gameAssembly.modBaseAddr, this.offsets.gameCode) - ); - if (newGameCode) { - const split = newGameCode.split('\r\n'); - if (split.length === 2) { - newGameCode = split[1]; - } else { - newGameCode = ''; - } - if (!/^[A-Z]{6}$/.test(newGameCode) || newGameCode === 'MENU') { - newGameCode = ''; - } - } - // console.log(this.gameCode, newGameCode); - } else if (inGame) { - newGameCode = ''; - } - if (newGameCode) this.gameCode = newGameCode; const newState = { - lobbyCode: this.gameCode, + lobbyCode: this.gameCode || 'MENU', players, gameState: state, oldGameState: this.oldGameState @@ -168,13 +152,17 @@ export default class GameReader { } } - constructor(reply: (event: string, ...args: unknown[]) => void, offsets: IOffsets) { + constructor(reply: (event: string, ...args: unknown[]) => void, offsets: IOffsetsContainer) { this.reply = reply; - this.offsets = offsets; + this.offsetsContainer = offsets; + } + initializeoffsets() { + this.is_64bit = this.isX64Version(); + this.offsets = this.is_64bit ? this.offsetsContainer.x64 : this.offsetsContainer.x86; this.PlayerStruct = new Struct(); - for (const member of offsets.player.struct) { + for (const member of this.offsets.player.struct) { if (member.type === 'SKIP' && member.skip) { this.PlayerStruct = this.PlayerStruct.addMember(Struct.TYPES.SKIP(member.skip), member.name); } else { @@ -182,11 +170,33 @@ export default class GameReader { } } + const gameClient = this.findPattern(this.offsets.signatures.gameclient.sig, this.offsets.signatures.gameclient.patternOffset, this.offsets.signatures.gameclient.addressOffset); + const meetingHud = this.findPattern(this.offsets.signatures.meetingHud.sig, this.offsets.signatures.meetingHud.patternOffset, this.offsets.signatures.meetingHud.addressOffset); + const gameData = this.findPattern(this.offsets.signatures.gameData.sig, this.offsets.signatures.gameData.patternOffset, this.offsets.signatures.gameData.addressOffset); + + this.offsets.meetingHud[0] = meetingHud; + this.offsets.exiledPlayerId[1] = meetingHud; + this.offsets.allPlayersPtr[0] = gameData; + this.offsets.gameState[0] = gameClient; + this.offsets.gameCode[0] = gameClient; + } + + isX64Version(): boolean { + if (!this.amongUs || !this.gameAssembly) + return false; + const optionalHeader_offset = readMemoryRaw(this.amongUs.handle, this.gameAssembly.modBaseAddr + 0x3C, 'uint32'); + const optionalHeader_magic = readMemoryRaw(this.amongUs.handle, this.gameAssembly.modBaseAddr + optionalHeader_offset + 0x18, 'short'); + console.log(optionalHeader_offset.toString(16), optionalHeader_magic.toString(16), (optionalHeader_offset + 0x18).toString(16)) + return optionalHeader_magic === 0x20B; + } readMemory(dataType: DataType, address: number, offsets: number[], defaultParam?: T): T { if (!this.amongUs) return defaultParam as T; if (address === 0) return defaultParam as T; + if (this.is_64bit && (dataType == 'pointer' || dataType == 'ptr')) { + dataType = 'uint64'; + } const { address: addr, last } = this.offsetAddress(address, offsets); if (addr === 0) return defaultParam as T; return readMemoryRaw( @@ -195,36 +205,71 @@ export default class GameReader { dataType ); } + offsetAddress(address: number, offsets: number[]): { address: number, last: number } { if (!this.amongUs) throw 'Among Us not open? Weird error'; - address = address & 0xffffffff; + address = this.is_64bit ? address : address & 0xffffffff; for (let i = 0; i < offsets.length - 1; i++) { - address = readMemoryRaw(this.amongUs.handle, address + offsets[i], 'uint32'); + address = readMemoryRaw(this.amongUs.handle, address + offsets[i], this.is_64bit ? 'uint64' : 'uint32'); if (address == 0) break; } const last = offsets.length > 0 ? offsets[offsets.length - 1] : 0; return { address, last }; } + readString(address: number): string { if (address === 0 || !this.amongUs) return ''; - const length = readMemoryRaw(this.amongUs.handle, address + 0x8, 'int'); - const buffer = readBuffer(this.amongUs.handle, address + 0xC, length << 1); - return buffer.toString('utf8').replace(/\0/g, ''); + const length = readMemoryRaw(this.amongUs.handle, address + (this.is_64bit ? 0x10 : 0x8), 'int'); + const buffer = readBuffer(this.amongUs.handle, address + (this.is_64bit ? 0x14 : 0xC), length << 1); + return buffer.toString('binary').replace(/\0/g, ''); } - parsePlayer(ptr: number, buffer: Buffer): Player { + findPattern(signature: string, patternOffset: number = 0x1, addressOffset: number = 0x0): number { + if (!this.amongUs || !this.gameAssembly) return 0x0; + const signatureTypes = 0x0 | 0x2; + const gameclient_function = findPatternRaw(this.amongUs.handle, "GameAssembly.dll", signature, signatureTypes, patternOffset, 0x0); + const offsetAddr = this.readMemory('int', this.gameAssembly.modBaseAddr, [gameclient_function]); + return this.is_64bit ? offsetAddr + gameclient_function + addressOffset : offsetAddr - this.gameAssembly.modBaseAddr;; + } + + IntToGameCode(input: number) { + if (input === 0 || input > -1000) + return ""; + + const V2: string = "QWXRTYLPESDFGHUJKZOCVBINMA"; + const a = input & 0x3FF; + const b = (input >> 10) & 0xFFFFF; + return [ + V2[Math.floor(a % 26)], + V2[Math.floor(a / 26)], + V2[Math.floor(b % 26)], + V2[Math.floor(b / 26 % 26)], + V2[Math.floor(b / (26 * 26) % 26)], + V2[Math.floor(b / (26 * 26 * 26) % 26)] + ].join(""); + } + + parsePlayer(ptr: number, buffer: Buffer): Player | undefined { + if (!this.PlayerStruct || !this.offsets) + return undefined; + const { data } = this.PlayerStruct.report(buffer, 0, {}); + if (this.is_64bit) { + data.objectPtr = this.readMemory('pointer', ptr, [this.PlayerStruct.getOffsetByName("objectPtr")]); + data.name = this.readMemory('pointer', ptr, [this.PlayerStruct.getOffsetByName("name")]); + } + const isLocal = this.readMemory('int', data.objectPtr, this.offsets.player.isLocal) !== 0; const positionOffsets = isLocal ? [ this.offsets.player.localX, this.offsets.player.localY ] : [ - this.offsets.player.remoteX, - this.offsets.player.remoteY - ]; + this.offsets.player.remoteX, + this.offsets.player.remoteY + ]; const x = this.readMemory('float', data.objectPtr, positionOffsets[0]); const y = this.readMemory('float', data.objectPtr, positionOffsets[1]); diff --git a/src/main/IOffsets.d.ts b/src/main/IOffsets.d.ts index 9d730e22..6673009a 100644 --- a/src/main/IOffsets.d.ts +++ b/src/main/IOffsets.d.ts @@ -1,3 +1,15 @@ +interface ISignature { + sig: string; + addressOffset: number; + patternOffset: number; +} + + +export interface IOffsetsContainer { + x64: IOffsets; + x86: IOffsets; +} + export interface IOffsets { meetingHud: number[]; @@ -25,4 +37,9 @@ export interface IOffsets { name: string; }[]; }; + signatures: { + gameclient: ISignature + meetingHud: ISignature + gameData: ISignature + } } diff --git a/src/main/hook-ti.ts b/src/main/hook-ti.ts index fbd10bc7..6e5f8387 100644 --- a/src/main/hook-ti.ts +++ b/src/main/hook-ti.ts @@ -4,6 +4,12 @@ import * as t from 'ts-interface-checker'; // tslint:disable:object-literal-key-quotes +export const ISignature = t.iface([], { + 'sig': 'string', + 'addressOffset': 'number', + 'patternOffset': 'number' +}); + export const IOffsets = t.iface([], { 'meetingHud': t.array('number'), 'meetingHudCachePtr': t.array('number'), @@ -14,7 +20,9 @@ export const IOffsets = t.iface([], { 'playerCount': t.array('number'), 'playerAddrPtr': 'number', 'exiledPlayerId': t.array('number'), - 'gameCode': t.array('number'), + 'gameCode': t.array('number'), + 'hostId': t.opt(t.array('number')), + 'clientId': t.opt(t.array('number')), 'player': t.iface([], { 'isLocal': t.array('number'), 'localX': t.array('number'), @@ -30,9 +38,19 @@ export const IOffsets = t.iface([], { 'name': 'string', })), }), + 'signatures' : t.iface([], { + 'gameclient' : ISignature, + 'meetingHud' : ISignature, + 'gameData' : ISignature + }) +}); + +export const IOffsetsContainer = t.iface([], { + 'x64': IOffsets, + 'x86': IOffsets }); const exportedTypeSuite: t.ITypeSuite = { - IOffsets, + IOffsets, IOffsetsContainer }; export default exportedTypeSuite; diff --git a/src/main/hook.ts b/src/main/hook.ts index 7c40ba13..16057901 100644 --- a/src/main/hook.ts +++ b/src/main/hook.ts @@ -1,5 +1,5 @@ import { app, dialog, ipcMain } from 'electron'; -import path, { resolve } from 'path'; +import path from 'path'; import yml from 'js-yaml'; // import * as Struct from 'structron'; import { HKEY, enumerateValues } from 'registry-js'; @@ -12,9 +12,8 @@ import axios, { AxiosError } from 'axios'; import { createCheckers } from 'ts-interface-checker'; import TI from './hook-ti'; -import { existsSync, readFileSync } from 'fs'; -import { IOffsets } from './IOffsets'; -const { IOffsets } = createCheckers(TI); +import { IOffsetsContainer } from './IOffsets'; +const { IOffsetsContainer } = createCheckers(TI); interface IOHookEvent { type: string @@ -29,39 +28,24 @@ interface IOHookEvent { const store = new Store(); -async function loadOffsets(event: Electron.IpcMainEvent): Promise { - - const valuesFile = resolve((process.env.LOCALAPPDATA || '') + 'Low', 'Innersloth/Among Us/Unity/6b8b0d91-4a20-4a00-a3e4-4da4a883a5f0/Analytics/values'); - let version = ''; - if (existsSync(valuesFile)) { - try { - const json = JSON.parse(readFileSync(valuesFile, 'utf8')); - version = json.app_ver; - } catch (e) { - console.error(e); - event.reply('error', `Couldn't determine the Among Us version - ${e}. Try opening Among Us and then restarting CrewLink.`); - return; - } - } else { - event.reply('error', 'Couldn\'t determine the Among Us version - Unity analytics file doesn\'t exist. Try opening Among Us and then restarting CrewLink.'); - return; - } - +async function loadOffsets(event: Electron.IpcMainEvent): Promise { + let version = 'x64' let data: string; const offsetStore = store.get('offsets') || {}; - if (version === offsetStore.version) { + if (false && version === offsetStore.version) { data = offsetStore.data; } else { try { const response = await axios({ - url: `${store.get('serverURL')}/${version}.yml` + // url: `${store.get('serverURL')}/offsets.yml` + url: `http://crewlink.guus.ninja/offsets.yml` }); data = response.data; } catch (_e) { const e = _e as AxiosError; console.error(e); if (e?.response?.status === 404) { - event.reply('error', `You are on an unsupported version of Among Us: ${version}.\n`); + event.reply('error', `You are on an unsupported voice server: ${version}.\n`); } else { let errorMessage = e.message; if (errorMessage.includes('ETIMEDOUT')) { @@ -77,9 +61,9 @@ async function loadOffsets(event: Electron.IpcMainEvent): Promise { ipcMain.on('initState', (event: Electron.IpcMainEvent) => { event.returnValue = gameReader.lastState; }); + const frame = () => { gameReader.loop(); setTimeout(frame, 1000 / 20); diff --git a/src/main/memoryjs.d.ts b/src/main/memoryjs.d.ts index 42679025..8357e1dd 100644 --- a/src/main/memoryjs.d.ts +++ b/src/main/memoryjs.d.ts @@ -46,6 +46,8 @@ declare module 'memoryjs' { export function writeBuffer(handle: number, address: number, buffer: Buffer): void; + export function findPattern(handle: number, moduleName: string, signature: string, signatureType: number , patternOffset: number, addressOffset: number): number; + // Functions // export enum ArgType { T_VOID, T_STRING, T_CHAR, T_BOOL, T_INT, T_DOUBLE, T_FLOAT } From 7497f401415ae70d5fbf46d487e482c8be00af94 Mon Sep 17 00:00:00 2001 From: OhMyGuus Date: Tue, 22 Dec 2020 02:52:18 +0100 Subject: [PATCH 27/88] - Removed log --- src/main/GameReader.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/GameReader.ts b/src/main/GameReader.ts index aa6af552..eddc4609 100644 --- a/src/main/GameReader.ts +++ b/src/main/GameReader.ts @@ -184,11 +184,10 @@ export default class GameReader { isX64Version(): boolean { if (!this.amongUs || !this.gameAssembly) return false; + const optionalHeader_offset = readMemoryRaw(this.amongUs.handle, this.gameAssembly.modBaseAddr + 0x3C, 'uint32'); const optionalHeader_magic = readMemoryRaw(this.amongUs.handle, this.gameAssembly.modBaseAddr + optionalHeader_offset + 0x18, 'short'); - console.log(optionalHeader_offset.toString(16), optionalHeader_magic.toString(16), (optionalHeader_offset + 0x18).toString(16)) return optionalHeader_magic === 0x20B; - } readMemory(dataType: DataType, address: number, offsets: number[], defaultParam?: T): T { From eb6eb998c760903b772afa82f7b4827e0ab71eca Mon Sep 17 00:00:00 2001 From: OhMyGuus Date: Tue, 22 Dec 2020 03:04:26 +0100 Subject: [PATCH 28/88] Removed debug crewlink server --- src/main/GameReader.ts | 4 ++-- src/main/hook.ts | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/GameReader.ts b/src/main/GameReader.ts index eddc4609..97e042f4 100644 --- a/src/main/GameReader.ts +++ b/src/main/GameReader.ts @@ -89,7 +89,7 @@ export default class GameReader { state = GameState.TASKS; break; } - this.gameCode = this.IntToGameCode(this.readMemory('int32', this.gameAssembly.modBaseAddr, this.offsets.gameCode)) + this.gameCode = this.IntToGameCode(this.readMemory('int32', this.gameAssembly.modBaseAddr, this.offsets.gameCode)); const allPlayersPtr = this.readMemory('ptr', this.gameAssembly.modBaseAddr, this.offsets.allPlayersPtr); const allPlayers = this.readMemory('ptr', allPlayersPtr, this.offsets.allPlayers); const playerCount = this.readMemory('int' as const, allPlayersPtr, this.offsets.playerCount); @@ -184,7 +184,7 @@ export default class GameReader { isX64Version(): boolean { if (!this.amongUs || !this.gameAssembly) return false; - + const optionalHeader_offset = readMemoryRaw(this.amongUs.handle, this.gameAssembly.modBaseAddr + 0x3C, 'uint32'); const optionalHeader_magic = readMemoryRaw(this.amongUs.handle, this.gameAssembly.modBaseAddr + optionalHeader_offset + 0x18, 'short'); return optionalHeader_magic === 0x20B; diff --git a/src/main/hook.ts b/src/main/hook.ts index 16057901..62ce7f15 100644 --- a/src/main/hook.ts +++ b/src/main/hook.ts @@ -29,16 +29,15 @@ interface IOHookEvent { const store = new Store(); async function loadOffsets(event: Electron.IpcMainEvent): Promise { - let version = 'x64' + let version = 'offsets_new' let data: string; const offsetStore = store.get('offsets') || {}; - if (false && version === offsetStore.version) { + if (version === offsetStore.version) { data = offsetStore.data; } else { try { const response = await axios({ - // url: `${store.get('serverURL')}/offsets.yml` - url: `http://crewlink.guus.ninja/offsets.yml` + url: `${store.get('serverURL')}/offsets.yml` }); data = response.data; } catch (_e) { From 1bf95cb4c9e3fd4fb8334eb77878725562b5b0f7 Mon Sep 17 00:00:00 2001 From: OhMyGuus Date: Tue, 22 Dec 2020 08:53:18 +0100 Subject: [PATCH 29/88] Renamed gaemclient to InnterNetClient (offical name) - Fix naming for the findpattern function --- src/main/GameReader.ts | 12 ++++++------ src/main/IOffsets.d.ts | 2 +- src/main/hook-ti.ts | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/GameReader.ts b/src/main/GameReader.ts index 97e042f4..92b79ebf 100644 --- a/src/main/GameReader.ts +++ b/src/main/GameReader.ts @@ -170,15 +170,15 @@ export default class GameReader { } } - const gameClient = this.findPattern(this.offsets.signatures.gameclient.sig, this.offsets.signatures.gameclient.patternOffset, this.offsets.signatures.gameclient.addressOffset); + const innerNetClient = this.findPattern(this.offsets.signatures.innerNetClient.sig, this.offsets.signatures.innerNetClient.patternOffset, this.offsets.signatures.innerNetClient.addressOffset); const meetingHud = this.findPattern(this.offsets.signatures.meetingHud.sig, this.offsets.signatures.meetingHud.patternOffset, this.offsets.signatures.meetingHud.addressOffset); const gameData = this.findPattern(this.offsets.signatures.gameData.sig, this.offsets.signatures.gameData.patternOffset, this.offsets.signatures.gameData.addressOffset); this.offsets.meetingHud[0] = meetingHud; this.offsets.exiledPlayerId[1] = meetingHud; this.offsets.allPlayersPtr[0] = gameData; - this.offsets.gameState[0] = gameClient; - this.offsets.gameCode[0] = gameClient; + this.offsets.gameState[0] = innerNetClient; + this.offsets.gameCode[0] = innerNetClient; } isX64Version(): boolean { @@ -227,9 +227,9 @@ export default class GameReader { findPattern(signature: string, patternOffset: number = 0x1, addressOffset: number = 0x0): number { if (!this.amongUs || !this.gameAssembly) return 0x0; const signatureTypes = 0x0 | 0x2; - const gameclient_function = findPatternRaw(this.amongUs.handle, "GameAssembly.dll", signature, signatureTypes, patternOffset, 0x0); - const offsetAddr = this.readMemory('int', this.gameAssembly.modBaseAddr, [gameclient_function]); - return this.is_64bit ? offsetAddr + gameclient_function + addressOffset : offsetAddr - this.gameAssembly.modBaseAddr;; + const instruction_location = findPatternRaw(this.amongUs.handle, "GameAssembly.dll", signature, signatureTypes, patternOffset, 0x0); + const offsetAddr = this.readMemory('int', this.gameAssembly.modBaseAddr, [instruction_location]); + return this.is_64bit ? offsetAddr + instruction_location + addressOffset : offsetAddr - this.gameAssembly.modBaseAddr;; } IntToGameCode(input: number) { diff --git a/src/main/IOffsets.d.ts b/src/main/IOffsets.d.ts index 6673009a..8cf9e6d9 100644 --- a/src/main/IOffsets.d.ts +++ b/src/main/IOffsets.d.ts @@ -38,7 +38,7 @@ export interface IOffsets { }[]; }; signatures: { - gameclient: ISignature + innerNetClient: ISignature meetingHud: ISignature gameData: ISignature } diff --git a/src/main/hook-ti.ts b/src/main/hook-ti.ts index 6e5f8387..3e865625 100644 --- a/src/main/hook-ti.ts +++ b/src/main/hook-ti.ts @@ -39,7 +39,7 @@ export const IOffsets = t.iface([], { })), }), 'signatures' : t.iface([], { - 'gameclient' : ISignature, + 'innerNetClient' : ISignature, 'meetingHud' : ISignature, 'gameData' : ISignature }) From 2b9cf88ec6ab47337a7dbb6d53fe15f231727df9 Mon Sep 17 00:00:00 2001 From: OhMyGuus Date: Tue, 22 Dec 2020 13:51:05 +0100 Subject: [PATCH 30/88] Added HostId & clientId --- src/common/AmongUsState.ts | 5 +++++ src/main/GameReader.ts | 13 ++++++++++++- src/main/IOffsets.d.ts | 3 +++ src/main/hook-ti.ts | 5 +++-- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/common/AmongUsState.ts b/src/common/AmongUsState.ts index 17d7301d..9a35a17a 100644 --- a/src/common/AmongUsState.ts +++ b/src/common/AmongUsState.ts @@ -4,10 +4,14 @@ export interface AmongUsState { oldGameState: GameState; lobbyCode: string; players: Player[]; + clientId: number; + hostId: number; } + export interface Player { ptr: number; id: number; + clientId: number; name: string; colorId: number; hatId: number; @@ -24,6 +28,7 @@ export interface Player { y: number; inVent: boolean; } + export enum GameState { LOBBY, TASKS, DISCUSSION, MENU, UNKNOWN } diff --git a/src/main/GameReader.ts b/src/main/GameReader.ts index 92b79ebf..6a21af0e 100644 --- a/src/main/GameReader.ts +++ b/src/main/GameReader.ts @@ -89,6 +89,7 @@ export default class GameReader { state = GameState.TASKS; break; } + this.gameCode = this.IntToGameCode(this.readMemory('int32', this.gameAssembly.modBaseAddr, this.offsets.gameCode)); const allPlayersPtr = this.readMemory('ptr', this.gameAssembly.modBaseAddr, this.offsets.allPlayersPtr); const allPlayers = this.readMemory('ptr', allPlayersPtr, this.offsets.allPlayers); @@ -133,11 +134,18 @@ export default class GameReader { this.lastPlayerPtr = allPlayers; + const hostId = this.readMemory('uint32', this.gameAssembly.modBaseAddr, this.offsets.hostId); + const clientId = this.readMemory('uint32', this.gameAssembly.modBaseAddr, this.offsets.clientId); + + const newState = { lobbyCode: this.gameCode || 'MENU', players, gameState: state, - oldGameState: this.oldGameState + oldGameState: this.oldGameState, + isHost: (hostId && clientId && hostId === clientId) as boolean, + hostId: hostId, + clientId: clientId }; const patch = patcher.diff(this.lastState, newState); if (patch) { @@ -272,9 +280,12 @@ export default class GameReader { const x = this.readMemory('float', data.objectPtr, positionOffsets[0]); const y = this.readMemory('float', data.objectPtr, positionOffsets[1]); + const clientId = this.readMemory('uint32', data.objectPtr, this.offsets.player.clientId); + return { ptr, id: data.id, + clientId: clientId, name: this.readString(data.name), colorId: data.color, hatId: data.hat, diff --git a/src/main/IOffsets.d.ts b/src/main/IOffsets.d.ts index 8cf9e6d9..8fb30a3d 100644 --- a/src/main/IOffsets.d.ts +++ b/src/main/IOffsets.d.ts @@ -22,6 +22,8 @@ export interface IOffsets { playerAddrPtr: number; exiledPlayerId: number[]; gameCode: number[]; + hostId: number[]; + clientId: number[]; player: { isLocal: number[]; localX: number[]; @@ -31,6 +33,7 @@ export interface IOffsets { bufferLength: number; offsets: number[]; inVent: number[]; + clientId: number[]; struct: { type: 'INT' | 'INT_BE' | 'UINT' | 'UINT_BE' | 'SHORT' | 'SHORT_BE' | 'USHORT' | 'USHORT_BE' | 'FLOAT' | 'CHAR' | 'BYTE' | 'SKIP'; skip?: number; diff --git a/src/main/hook-ti.ts b/src/main/hook-ti.ts index 3e865625..bf83af86 100644 --- a/src/main/hook-ti.ts +++ b/src/main/hook-ti.ts @@ -21,8 +21,8 @@ export const IOffsets = t.iface([], { 'playerAddrPtr': 'number', 'exiledPlayerId': t.array('number'), 'gameCode': t.array('number'), - 'hostId': t.opt(t.array('number')), - 'clientId': t.opt(t.array('number')), + 'hostId': t.array('number'), + 'clientId': t.array('number'), 'player': t.iface([], { 'isLocal': t.array('number'), 'localX': t.array('number'), @@ -31,6 +31,7 @@ export const IOffsets = t.iface([], { 'remoteY': t.array('number'), 'offsets': t.array('number'), 'inVent': t.array('number'), + 'clientId': t.array('number'), 'bufferLength': 'number', 'struct': t.array(t.iface([], { 'type': 'string', From 6a207bdd238e1e9b78db7335c994c42dfcc50d9f Mon Sep 17 00:00:00 2001 From: OhMyGuus Date: Tue, 22 Dec 2020 14:11:03 +0100 Subject: [PATCH 31/88] Added IsHost to the state --- src/common/AmongUsState.ts | 1 + src/main/GameReader.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/common/AmongUsState.ts b/src/common/AmongUsState.ts index 9a35a17a..9fd6bd04 100644 --- a/src/common/AmongUsState.ts +++ b/src/common/AmongUsState.ts @@ -6,6 +6,7 @@ export interface AmongUsState { players: Player[]; clientId: number; hostId: number; + isHost: boolean; } export interface Player { diff --git a/src/main/GameReader.ts b/src/main/GameReader.ts index 6a21af0e..0e9ec169 100644 --- a/src/main/GameReader.ts +++ b/src/main/GameReader.ts @@ -138,7 +138,7 @@ export default class GameReader { const clientId = this.readMemory('uint32', this.gameAssembly.modBaseAddr, this.offsets.clientId); - const newState = { + const newState : AmongUsState = { lobbyCode: this.gameCode || 'MENU', players, gameState: state, From c6ace97b7e52ec6e5b2b97c2ba14d0d641bddc43 Mon Sep 17 00:00:00 2001 From: OhMyGuus Date: Tue, 22 Dec 2020 16:09:31 +0100 Subject: [PATCH 32/88] - Small fix for the size of the playeraddress --- src/main/GameReader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/GameReader.ts b/src/main/GameReader.ts index 0e9ec169..bf200d6d 100644 --- a/src/main/GameReader.ts +++ b/src/main/GameReader.ts @@ -105,7 +105,7 @@ export default class GameReader { const { address, last } = this.offsetAddress(playerAddrPtr, this.offsets.player.offsets); const playerData = readBuffer(this.amongUs.handle, address + last, this.offsets.player.bufferLength); const player = this.parsePlayer(address + last, playerData); - playerAddrPtr += 4; + playerAddrPtr += this.is_64bit? 8 : 4; if (!player) continue; players.push(player); From c1b05ce795a1ca78096b480c49799c4a9f0e8293 Mon Sep 17 00:00:00 2001 From: OhMyGuus Date: Wed, 23 Dec 2020 10:26:56 +0100 Subject: [PATCH 33/88] Small fix for gamecode. --- src/main/GameReader.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/GameReader.ts b/src/main/GameReader.ts index bf200d6d..799af4c7 100644 --- a/src/main/GameReader.ts +++ b/src/main/GameReader.ts @@ -90,7 +90,7 @@ export default class GameReader { break; } - this.gameCode = this.IntToGameCode(this.readMemory('int32', this.gameAssembly.modBaseAddr, this.offsets.gameCode)); + this.gameCode = state === GameState.MENU? "" : this.IntToGameCode(this.readMemory('int32', this.gameAssembly.modBaseAddr, this.offsets.gameCode)); const allPlayersPtr = this.readMemory('ptr', this.gameAssembly.modBaseAddr, this.offsets.allPlayersPtr); const allPlayers = this.readMemory('ptr', allPlayersPtr, this.offsets.allPlayers); const playerCount = this.readMemory('int' as const, allPlayersPtr, this.offsets.playerCount); @@ -241,7 +241,7 @@ export default class GameReader { } IntToGameCode(input: number) { - if (input === 0 || input > -1000) + if (!input || input === 0 || input > -1000) return ""; const V2: string = "QWXRTYLPESDFGHUJKZOCVBINMA"; From f5dac29cc6aad01a7f5ba906b81d49725a3f89e6 Mon Sep 17 00:00:00 2001 From: OhMyGuus Date: Wed, 23 Dec 2020 16:47:36 +0100 Subject: [PATCH 34/88] Fixed lint errors --- src/main/GameReader.ts | 66 +++++++++++++++++++++--------------------- src/main/hook-ti.ts | 6 ++-- src/main/hook.ts | 4 +-- 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/main/GameReader.ts b/src/main/GameReader.ts index 799af4c7..b933882c 100644 --- a/src/main/GameReader.ts +++ b/src/main/GameReader.ts @@ -34,7 +34,7 @@ export default class GameReader { lastPlayerPtr = 0; shouldReadLobby = false; exileCausesEnd = false; - is_64bit: boolean = false; + is_64bit = false; oldGameState = GameState.UNKNOWN; lastState: AmongUsState = {} as AmongUsState; amongUs: ProcessObject | null = null; @@ -71,26 +71,26 @@ export default class GameReader { const gameState = this.readMemory('int', this.gameAssembly.modBaseAddr, this.offsets.gameState); switch (gameState) { - case 0: - state = GameState.MENU; - this.exileCausesEnd = false; - break; - case 1: - case 3: + case 0: + state = GameState.MENU; + this.exileCausesEnd = false; + break; + case 1: + case 3: + state = GameState.LOBBY; + this.exileCausesEnd = false; + break; + default: + if (this.exileCausesEnd) state = GameState.LOBBY; - this.exileCausesEnd = false; - break; - default: - if (this.exileCausesEnd) - state = GameState.LOBBY; - else if (meetingHudState < 4) - state = GameState.DISCUSSION; - else - state = GameState.TASKS; - break; + else if (meetingHudState < 4) + state = GameState.DISCUSSION; + else + state = GameState.TASKS; + break; } - this.gameCode = state === GameState.MENU? "" : this.IntToGameCode(this.readMemory('int32', this.gameAssembly.modBaseAddr, this.offsets.gameCode)); + this.gameCode = state === GameState.MENU ? '' : this.IntToGameCode(this.readMemory('int32', this.gameAssembly.modBaseAddr, this.offsets.gameCode)); const allPlayersPtr = this.readMemory('ptr', this.gameAssembly.modBaseAddr, this.offsets.allPlayersPtr); const allPlayers = this.readMemory('ptr', allPlayersPtr, this.offsets.allPlayers); const playerCount = this.readMemory('int' as const, allPlayersPtr, this.offsets.playerCount); @@ -105,7 +105,7 @@ export default class GameReader { const { address, last } = this.offsetAddress(playerAddrPtr, this.offsets.player.offsets); const playerData = readBuffer(this.amongUs.handle, address + last, this.offsets.player.bufferLength); const player = this.parsePlayer(address + last, playerData); - playerAddrPtr += this.is_64bit? 8 : 4; + playerAddrPtr += this.is_64bit ? 8 : 4; if (!player) continue; players.push(player); @@ -138,7 +138,7 @@ export default class GameReader { const clientId = this.readMemory('uint32', this.gameAssembly.modBaseAddr, this.offsets.clientId); - const newState : AmongUsState = { + const newState: AmongUsState = { lobbyCode: this.gameCode || 'MENU', players, gameState: state, @@ -166,7 +166,7 @@ export default class GameReader { } - initializeoffsets() { + initializeoffsets() : void { this.is_64bit = this.isX64Version(); this.offsets = this.is_64bit ? this.offsetsContainer.x64 : this.offsetsContainer.x86; this.PlayerStruct = new Struct(); @@ -232,19 +232,19 @@ export default class GameReader { return buffer.toString('binary').replace(/\0/g, ''); } - findPattern(signature: string, patternOffset: number = 0x1, addressOffset: number = 0x0): number { + findPattern(signature: string, patternOffset = 0x1, addressOffset = 0x0): number { if (!this.amongUs || !this.gameAssembly) return 0x0; const signatureTypes = 0x0 | 0x2; - const instruction_location = findPatternRaw(this.amongUs.handle, "GameAssembly.dll", signature, signatureTypes, patternOffset, 0x0); + const instruction_location = findPatternRaw(this.amongUs.handle, 'GameAssembly.dll', signature, signatureTypes, patternOffset, 0x0); const offsetAddr = this.readMemory('int', this.gameAssembly.modBaseAddr, [instruction_location]); - return this.is_64bit ? offsetAddr + instruction_location + addressOffset : offsetAddr - this.gameAssembly.modBaseAddr;; + return this.is_64bit ? offsetAddr + instruction_location + addressOffset : offsetAddr - this.gameAssembly.modBaseAddr; } - IntToGameCode(input: number) { + IntToGameCode(input: number) : string { if (!input || input === 0 || input > -1000) - return ""; + return ''; - const V2: string = "QWXRTYLPESDFGHUJKZOCVBINMA"; + const V2 = 'QWXRTYLPESDFGHUJKZOCVBINMA'; const a = input & 0x3FF; const b = (input >> 10) & 0xFFFFF; return [ @@ -254,7 +254,7 @@ export default class GameReader { V2[Math.floor(b / 26 % 26)], V2[Math.floor(b / (26 * 26) % 26)], V2[Math.floor(b / (26 * 26 * 26) % 26)] - ].join(""); + ].join(''); } parsePlayer(ptr: number, buffer: Buffer): Player | undefined { @@ -264,8 +264,8 @@ export default class GameReader { const { data } = this.PlayerStruct.report(buffer, 0, {}); if (this.is_64bit) { - data.objectPtr = this.readMemory('pointer', ptr, [this.PlayerStruct.getOffsetByName("objectPtr")]); - data.name = this.readMemory('pointer', ptr, [this.PlayerStruct.getOffsetByName("name")]); + data.objectPtr = this.readMemory('pointer', ptr, [this.PlayerStruct.getOffsetByName('objectPtr')]); + data.name = this.readMemory('pointer', ptr, [this.PlayerStruct.getOffsetByName('name')]); } const isLocal = this.readMemory('int', data.objectPtr, this.offsets.player.isLocal) !== 0; @@ -274,9 +274,9 @@ export default class GameReader { this.offsets.player.localX, this.offsets.player.localY ] : [ - this.offsets.player.remoteX, - this.offsets.player.remoteY - ]; + this.offsets.player.remoteX, + this.offsets.player.remoteY + ]; const x = this.readMemory('float', data.objectPtr, positionOffsets[0]); const y = this.readMemory('float', data.objectPtr, positionOffsets[1]); diff --git a/src/main/hook-ti.ts b/src/main/hook-ti.ts index bf83af86..e8985143 100644 --- a/src/main/hook-ti.ts +++ b/src/main/hook-ti.ts @@ -20,9 +20,9 @@ export const IOffsets = t.iface([], { 'playerCount': t.array('number'), 'playerAddrPtr': 'number', 'exiledPlayerId': t.array('number'), - 'gameCode': t.array('number'), - 'hostId': t.array('number'), - 'clientId': t.array('number'), + 'gameCode': t.array('number'), + 'hostId': t.array('number'), + 'clientId': t.array('number'), 'player': t.iface([], { 'isLocal': t.array('number'), 'localX': t.array('number'), diff --git a/src/main/hook.ts b/src/main/hook.ts index 62ce7f15..f735f932 100644 --- a/src/main/hook.ts +++ b/src/main/hook.ts @@ -29,7 +29,7 @@ interface IOHookEvent { const store = new Store(); async function loadOffsets(event: Electron.IpcMainEvent): Promise { - let version = 'offsets_new' + const version = 'offsets_new'; let data: string; const offsetStore = store.get('offsets') || {}; if (version === offsetStore.version) { @@ -37,7 +37,7 @@ async function loadOffsets(event: Electron.IpcMainEvent): Promise Date: Thu, 24 Dec 2020 01:34:30 +0100 Subject: [PATCH 35/88] Fixed hostid,clientid forgot to add it. --- src/main/GameReader.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/GameReader.ts b/src/main/GameReader.ts index b933882c..21a0317a 100644 --- a/src/main/GameReader.ts +++ b/src/main/GameReader.ts @@ -187,6 +187,9 @@ export default class GameReader { this.offsets.allPlayersPtr[0] = gameData; this.offsets.gameState[0] = innerNetClient; this.offsets.gameCode[0] = innerNetClient; + this.offsets.hostId[0] = innerNetClient; + this.offsets.clientId[0] = innerNetClient; + } isX64Version(): boolean { From a2f0955488cb484f0af8b119abbd59ab2a88782c Mon Sep 17 00:00:00 2001 From: Ottomated Date: Fri, 25 Dec 2020 19:35:30 -0800 Subject: [PATCH 36/88] toggle mute, fix errors --- src/common/ipc-messages.ts | 11 +++++++++++ src/renderer/settings/Settings.tsx | 7 +++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/common/ipc-messages.ts b/src/common/ipc-messages.ts index 7bd6cb9f..aae0dd9d 100644 --- a/src/common/ipc-messages.ts +++ b/src/common/ipc-messages.ts @@ -1,3 +1,5 @@ +import { ProgressInfo } from 'builder-util-runtime'; + // Renderer --> Main (send/on) export enum IpcMessages { SHOW_ERROR_DIALOG = 'SHOW_ERROR_DIALOG', @@ -21,5 +23,14 @@ export enum IpcRendererMessages { NOTIFY_GAME_OPENED = 'NOTIFY_GAME_OPENED', NOTIFY_GAME_STATE_CHANGED = 'NOTIFY_GAME_STATE_CHANGED', TOGGLE_DEAFEN = 'TOGGLE_DEAFEN', + TOGGLE_MUTE = 'TOGGLE_MUTE', PUSH_TO_TALK = 'PUSH_TO_TALK', + ERROR = 'ERROR', + AUTO_UPDATER_STATE = 'AUTO_UPDATER_STATE', } + +export interface AutoUpdaterState { + state: 'error' | 'available' | 'downloading' | 'downloaded' | 'unavailable'; + error?: string; + progress?: ProgressInfo; +} \ No newline at end of file diff --git a/src/renderer/settings/Settings.tsx b/src/renderer/settings/Settings.tsx index b5513525..261bcbb3 100644 --- a/src/renderer/settings/Settings.tsx +++ b/src/renderer/settings/Settings.tsx @@ -419,10 +419,9 @@ const Settings: React.FC = function ({ setLocalDistance(settings.localLobbySettings.maxDistance); }, [settings.localLobbySettings.maxDistance]); - const isInMenuOrLobby = gameState.gameState === GameState.LOBBY || gameState.gameState === GameState.MENU; - const canChangeLobbySettings = (gameState.gameState === GameState.MENU) || (gameState.isHost && gameState.gameState === GameState.LOBBY); + const isInMenuOrLobby = gameState?.gameState === GameState.LOBBY || gameState?.gameState === GameState.MENU; + const canChangeLobbySettings = (gameState?.gameState === GameState.MENU) || (gameState?.isHost && gameState?.gameState === GameState.LOBBY); - console.log(gameState); return (
@@ -478,7 +477,7 @@ const Settings: React.FC = function ({ type: 'setLobbySetting', action: ['maxDistance', newValue as number], }); - if (gameState.isHost) { + if (gameState?.isHost) { setLobbySettings({ type: 'setOne', action: ['maxDistance', newValue as number], From 6f4d2242430ac4db7a1f5fcf2919fa149d086d14 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Fri, 25 Dec 2020 19:48:14 -0800 Subject: [PATCH 37/88] fix audio source not opening sometimes --- src/renderer/Voice.tsx | 14 ++++---------- src/renderer/settings/MicrophoneSoundBar.tsx | 2 +- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index 788007c9..95401e5a 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -207,7 +207,6 @@ const Voice: React.FC = function ({ error }: VoiceProps) { const [deafenedState, setDeafened] = useState(false); const [mutedState, setMuted] = useState(false); const [connected, setConnected] = useState(false); - function disconnectPeer(peer: string) { const connection = peerConnections[peer]; if (!connection) { @@ -312,16 +311,11 @@ const Voice: React.FC = function ({ error }: VoiceProps) { autoGainControl: false, googAutoGainControl: false, googAutoGainControl2: false, - channelCount: 2, - latency: 0, - sampleRate: 48000, - sampleSize: 16, }; // Get microphone settings - if (settings.microphone.toLowerCase() !== 'default') - audio.deviceId = settings.microphone; - + if (settingsRef.current.microphone.toLowerCase() !== 'default') + audio.deviceId = settingsRef.current.microphone; navigator.getUserMedia( { video: false, audio }, async (stream) => { @@ -418,8 +412,8 @@ const Voice: React.FC = function ({ error }: VoiceProps) { ) as ExtendedAudioElement; document.body.appendChild(audio); audio.srcObject = stream; - if (settings.speaker.toLowerCase() !== 'default') - audio.setSinkId(settings.speaker); + if (settingsRef.current.speaker.toLowerCase() !== 'default') + audio.setSinkId(settingsRef.current.speaker); const context = new AudioContext(); const source = context.createMediaStreamSource(stream); diff --git a/src/renderer/settings/MicrophoneSoundBar.tsx b/src/renderer/settings/MicrophoneSoundBar.tsx index 53ce0991..ac7e0eae 100644 --- a/src/renderer/settings/MicrophoneSoundBar.tsx +++ b/src/renderer/settings/MicrophoneSoundBar.tsx @@ -56,7 +56,7 @@ const TestMicrophoneButton: React.FC = function ({ }; navigator.mediaDevices - .getUserMedia({ audio: { deviceId: microphone ?? 'default' } }) + .getUserMedia({ audio: { deviceId: microphone ?? 'default' }, video: false }) .then((stream) => { const src = ctx.createMediaStreamSource(stream); src.connect(processor); From 0b501fc6cba256eed44e901058abb75e738fab74 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Fri, 25 Dec 2020 21:17:12 -0800 Subject: [PATCH 38/88] Auto-updates in-app --- package.json | 2 ++ src/main/index.ts | 30 ++++++++++++++++++++++++++++++ src/renderer/App.tsx | 29 ++++++++++++++++++++++++++--- src/renderer/theme.ts | 1 + yarn.lock | 12 ++++++++++++ 5 files changed, 71 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 46a55260..ec6ad38c 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "electron-window-state": "^5.0.3", "iohook": "^0.7.1", "memoryjs": "git://github.com/Rob--/memoryjs", + "pretty-bytes": "^5.5.0", "react": "^17.0.1", "react-dom": "^17.0.1", "registry-js": "^1.12.0", @@ -68,6 +69,7 @@ "@types/deep-equal": "^1.0.1", "@types/js-yaml": "^3.12.5", "@types/node": "12", + "@types/pretty-bytes": "^5.2.0", "@types/react": "^16.9.53", "@types/react-dom": "^16.9.8", "@types/simple-peer": "^9.6.1", diff --git a/src/main/index.ts b/src/main/index.ts index 18c662f0..4eac02b1 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -107,8 +107,38 @@ if (!gotTheLock) { mainWindow?.webContents.send(IpcRendererMessages.AUTO_UPDATER_STATE, { state: 'downloaded', }); + app.relaunch(); + autoUpdater.quitAndInstall(); }); + // Mock auto-update download + // setTimeout(() => { + // mainWindow?.webContents.send(IpcRendererMessages.AUTO_UPDATER_STATE, { + // state: 'available' + // }); + // let total = 1000*1000; + // let i = 0; + // let interval = setInterval(() => { + // mainWindow?.webContents.send(IpcRendererMessages.AUTO_UPDATER_STATE, { + // state: 'downloading', + // progress: { + // total, + // delta: total * 0.01, + // transferred: i * total / 100, + // percent: i, + // bytesPerSecond: 1000 + // } + // } as AutoUpdaterState); + // i++; + // if (i === 100) { + // clearInterval(interval); + // mainWindow?.webContents.send(IpcRendererMessages.AUTO_UPDATER_STATE, { + // state: 'downloaded', + // }); + // } + // }, 100); + // }, 10000); + app.on('second-instance', () => { // Someone tried to run a second instance, we should focus our window. if (mainWindow) { diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 61cf48c5..a267544f 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -27,6 +27,13 @@ import CloseIcon from '@material-ui/icons/Close'; import IconButton from '@material-ui/core/IconButton'; import Dialog from '@material-ui/core/Dialog'; import makeStyles from '@material-ui/core/styles/makeStyles'; +import LinearProgress from '@material-ui/core/LinearProgress'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogContentText from '@material-ui/core/DialogContentText'; +import DialogActions from '@material-ui/core/DialogActions'; +import Button from '@material-ui/core/Button'; +import prettyBytes from 'pretty-bytes'; let appVersion = ''; if (typeof window !== 'undefined' && window.location) { @@ -133,7 +140,7 @@ function App() { setError(error); }; const onAutoUpdaterStateChange = (_: Electron.IpcRendererEvent, state: AutoUpdaterState) => { - setUpdaterState(state); + setUpdaterState(old => ({...old, ...state})); } let shouldInit = true; ipcRenderer.invoke(IpcHandlerMessages.START_HOOK).then(() => { @@ -168,6 +175,7 @@ function App() { page = ; break; } + console.log(updaterState); return ( @@ -182,8 +190,23 @@ function App() { open={settingsOpen} onClose={() => setSettingsOpen(false)} /> - - + + Installing Updates + + {((updaterState.state === 'downloading' || updaterState.state === 'downloaded') && updaterState.progress) && + <> + + {prettyBytes(updaterState.progress.transferred)} / {prettyBytes(updaterState.progress.total)} + + } + { + updaterState.state === 'error' && + {updaterState.error} + } + + {updaterState.state === 'error' && + + } {page} diff --git a/src/renderer/theme.ts b/src/renderer/theme.ts index ef77768f..777387e4 100644 --- a/src/renderer/theme.ts +++ b/src/renderer/theme.ts @@ -8,6 +8,7 @@ const theme = createMuiTheme({ secondary: red, background: { default: '#27232a', + paper: '#272727' }, type: 'dark', }, diff --git a/yarn.lock b/yarn.lock index b7618f15..da89c97a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1915,6 +1915,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.1.tgz#303f74c8a2b35644594139e948b2be470ae1186f" integrity sha512-/xaVmBBjOGh55WCqumLAHXU9VhjGtmyTGqJzFBXRWZzByOXI5JAJNx9xPVGEsNizrNwcec92fQMj458MWfjN1A== +"@types/pretty-bytes@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@types/pretty-bytes/-/pretty-bytes-5.2.0.tgz#857dcf4a21839e5bfb1c188dda62f986fdfa2348" + integrity sha512-dJhMFphDp6CE+OAZVyqzha9KsmgeqRMbZN4dIbMSrfObiuzfjucwKdn6zu+ttrjMwmz+Vz71/xXgHx5pO0axhA== + dependencies: + pretty-bytes "*" + "@types/prop-types@*": version "15.7.3" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" @@ -8474,6 +8481,11 @@ prettier@^2.0.0, prettier@^2.2.1: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5" integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q== +pretty-bytes@*, pretty-bytes@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.5.0.tgz#0cecda50a74a941589498011cf23275aa82b339e" + integrity sha512-p+T744ZyjjiaFlMUZZv6YPC5JrkNj8maRmPaQCWFJFplUAzpIUTRaTcS+7wmZtUoFXHtESJb23ISliaWyz3SHA== + pretty-bytes@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-1.0.4.tgz#0a22e8210609ad35542f8c8d5d2159aff0751c84" From a5d5bbbb64e4deb40b52fb080a83a7e5a78e8c34 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 26 Dec 2020 17:57:14 -0800 Subject: [PATCH 39/88] Open dialog when changing server URL --- src/renderer/settings/Settings.tsx | 120 ++++++++++++++++++++--------- src/renderer/theme.ts | 4 +- 2 files changed, 88 insertions(+), 36 deletions(-) diff --git a/src/renderer/settings/Settings.tsx b/src/renderer/settings/Settings.tsx index 261bcbb3..dfd35834 100644 --- a/src/renderer/settings/Settings.tsx +++ b/src/renderer/settings/Settings.tsx @@ -25,8 +25,12 @@ import IconButton from '@material-ui/core/IconButton'; import Alert from '@material-ui/lab/Alert'; import Slider from '@material-ui/core/Slider'; import Tooltip from '@material-ui/core/Tooltip'; +import Dialog from '@material-ui/core/Dialog'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogActions from '@material-ui/core/DialogActions'; import { GameState } from '../../common/AmongUsState'; -// import '../css/settings.css'; +import Button from '@material-ui/core/Button'; interface StyleInput { open: boolean; @@ -89,6 +93,15 @@ const useStyles = makeStyles((theme) => ({ bottom: theme.spacing(1), zIndex: 10, }, + urlDialog: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'start', + '&>*': { + marginBottom: theme.spacing(1) + } + } })); const keys = new Set([ @@ -256,53 +269,88 @@ interface MediaDevice { label: string; } + +function validateServerUrl(uri: string): boolean { + try { + if (uri.endsWith('/')) return false; + if (!isHttpUri(uri) && !isHttpsUri(uri)) return false; + const url = new URL(uri); + if (url.hostname === 'discord.gg') return false; + if (url.pathname !== '/') return false; + return true; + } catch (_) { + return false; + } +} + type URLInputProps = { initialURL: string; onValidURL: (url: string) => void; + className: string; }; -function validateServerUrl(uri: string): boolean { - if (uri.endsWith('/')) return false; - if (!isHttpUri(uri) && !isHttpsUri(uri)) return false; - const url = new URL(uri); - if (url.hostname === 'discord.gg') return false; - if (url.pathname !== '/') return false; - return true; -} - const URLInput: React.FC = function ({ initialURL, onValidURL, + className }: URLInputProps) { const [isValidURL, setURLValid] = useState(true); const [currentURL, setCurrentURL] = useState(initialURL); + const [open, setOpen] = useState(false); useEffect(() => { setCurrentURL(initialURL); }, [initialURL]); function handleChange(event: React.ChangeEvent) { - setCurrentURL(event.target.value); - - if (validateServerUrl(event.target.value)) { + let url = event.target.value.trim(); + if (url.endsWith('/')) url = url.substring(0, url.length - 1); + setCurrentURL(url); + console.log(url, validateServerUrl(url)); + if (validateServerUrl(url)) { setURLValid(true); - onValidURL(event.target.value); } else { setURLValid(false); } } return ( - + <> + + setOpen(false)}> + Change Voice Server + + + This option is for advanced users only. Untrusted servers can potentially steal your info or crash CrewLink. + + + + + + + + ); }; @@ -389,7 +437,7 @@ const Settings: React.FC = function ({ if (k === 'Control' || k === 'Alt' || k === 'Shift') k = (ev.location === 1 ? 'L' : 'R') + k; - if (/^[0-9A-Z]$/.test(k) || /^F[0-9]{1,2}$/.test(k) || keys.has(k)) { + if (/^[0-9A-Z]$/.test(k) || /^F[0-9]{1, 2}$/.test(k) || keys.has(k)) { setSettings({ type: 'setOne', action: [shortcut, k], @@ -444,16 +492,6 @@ const Settings: React.FC = function ({ Settings
- { - setSettings({ - type: 'setOne', - action: ['serverURL', url], - }); - }} - /> - {/* Lobby Settings */}
Lobby Settings @@ -488,6 +526,7 @@ const Settings: React.FC = function ({
+ Audio = function ({ + Advanced = function ({ }} control={} /> + { + setSettings({ + type: 'setOne', + action: ['serverURL', url], + }); + }} + className={classes.urlDialog} + /> Date: Sat, 26 Dec 2020 17:57:25 -0800 Subject: [PATCH 40/88] check! --- TODO.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/TODO.md b/TODO.md index af4086ed..d879ee43 100644 --- a/TODO.md +++ b/TODO.md @@ -6,7 +6,7 @@ - [x] Move the default server to a better host. - [x] Rewrite all error messages to be even more human-readable. - [ ] Integrate an official server list into the client. -- [ ] Detect the reason *why* the server can't provide offsets: i.e. Among Us just updated, it's an old version of Among Us, the server hasn't updated, etc. +- [x] Detect the reason *why* the server can't provide offsets: i.e. Among Us just updated, it's an old version of Among Us, the server hasn't updated, etc. ### Stretch @@ -18,10 +18,10 @@ - [ ] Add a microphone boost slider. - [ ] Add a speaker adjustment slider. - [ ] Add individual adjustment sliders to each of the players. -- [ ] Handle all RTC errors to make it unnecessary to ever re-open an RTC connection. +- [x] Handle all RTC errors to make it unnecessary to ever re-open an RTC connection. - [ ] Detect reason for RTC failure: NAT type, etc? -- [ ] Re-enable all `navigator.getUserMedia` functions that can be re-enabled with autoGainControl kicking in. -- [ ] Move all player-to-player communication logic to RTC data channels, versus sending them over the websocket. +- [x] Re-enable all `navigator.getUserMedia` functions that can be re-enabled with autoGainControl kicking in. +- [x] Move all player-to-player communication logic to RTC data channels, versus sending them over the websocket. ### Stretch @@ -29,9 +29,9 @@ ## Game Reader -- [ ] Fix unicode characters in player names +- [x] Fix unicode characters in player names - [ ] Indicate to the user when it can't read memory properly. Example: screen displays `MENU` while in lobby due to some misplaced offset. -- [ ] Don't use the Unity Analytics file to read the game version. Use either a hash of the GameAssembly dll, or DMA it from the process. +- [x] Don't use the Unity Analytics file to read the game version. Use either a hash of the GameAssembly dll, or DMA it from the process. ### Stretch From 3bf5682760eb5f8aafb32cd85cf14fc8cc79b027 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 26 Dec 2020 17:58:00 -0800 Subject: [PATCH 41/88] finish updater dialog --- src/renderer/App.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index a267544f..5ec45be5 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -176,7 +176,6 @@ function App() { break; } - console.log(updaterState); return ( @@ -191,7 +190,7 @@ function App() { onClose={() => setSettingsOpen(false)} /> - Installing Updates + Updating... {((updaterState.state === 'downloading' || updaterState.state === 'downloaded') && updaterState.progress) && <> From 493e4ab74bebf0395c6fffe521dffa218acdd805 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 26 Dec 2020 17:58:22 -0800 Subject: [PATCH 42/88] Remove css files --- src/renderer/Menu.tsx | 34 +++++++- src/renderer/Voice.tsx | 3 +- src/renderer/css/index.css | 146 +--------------------------------- src/renderer/css/menu.css | 64 --------------- src/renderer/css/settings.css | 110 ------------------------- 5 files changed, 33 insertions(+), 324 deletions(-) delete mode 100644 src/renderer/css/menu.css delete mode 100644 src/renderer/css/settings.css diff --git a/src/renderer/Menu.tsx b/src/renderer/Menu.tsx index ed7f65ba..3a7d3958 100644 --- a/src/renderer/Menu.tsx +++ b/src/renderer/Menu.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { ipcRenderer } from 'electron'; -import './css/menu.css'; import Footer from './Footer'; import { IpcMessages } from '../common/ipc-messages'; import makeStyles from '@material-ui/core/styles/makeStyles'; @@ -17,6 +16,33 @@ const useStyles = makeStyles((theme) => ({ error: { paddingTop: theme.spacing(4), }, + menu: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'start' + }, + waiting: { + fontSize: 20, + marginTop: 12, + marginBottom: 12 + }, + button: { + color: 'white', + background: 'none', + padding: '2px 10px', + borderRadius: 10, + border: '4px solid white', + fontSize: 24, + outline: 'none', + fontWeight: 500, + fontFamily: '"Varela", sans-serif', + marginTop: 24, + '&:hover': { + borderColor: '#00ff00', + cursor: 'pointer' + } + } })); export interface MenuProps { @@ -27,7 +53,7 @@ const Menu: React.FC = function ({ error }: MenuProps) { const classes = useStyles(); return (
-
+
{error ? (
@@ -40,10 +66,10 @@ const Menu: React.FC = function ({ error }: MenuProps) {
) : ( <> - Waiting for Among Us + Waiting for Among Us
- {gameState.lobbyCode &&
} + {gameState.lobbyCode && } svg { - position: absolute; - top: 10px; - right: 10px; - background: #ea3c2a; - border-radius: 50%; - padding: 2px; -} - -.username { - display: block; - text-align: center; - font-size: 20px; -} - -.username svg { - height: 20px; - margin-right: 3px; -} - -.code { - font-family: 'Source Code Pro', monospace; - display: block; - width: fit-content; - margin: 5px auto; - padding: 5px; - border-radius: 5px; - font-size: 28px; -} - -.titlebar { - position: absolute; - width: 100vw; - height: 24px; - background-color: black; - top: 0; - -webkit-app-region: drag; -} - -.titlebar-button { - -webkit-app-region: no-drag; - margin-left: auto; - padding: 0px; - position: absolute; - top: 2px; - border-radius: 50%; - cursor: pointer; -} - -.close { - right: 2px; -} - -.settings { - left: 2px; -} - -.canvas { - /* image-rendering: pixelated; */ - width: 100%; - /* transform: translateY(30%); */ -} - -.otherplayers { - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - width: '100%'; - flex-wrap: wrap; -} - -.otherplayers>* { - margin: 5px; -} - -.top { - display: flex; - justify-content: center; - align-items: center; -} - -.top>* { - margin: 5px; -} - -.right { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - -hr { - margin: 5px 10px; - border: 1px solid #313738; - border-radius: 2px; -} - -.react-tooltip-lite { - background: #23272A; - border: 2px solid white; - border-radius: 5px; -} - -.rainbow-text { - background-image: repeating-linear-gradient(90deg, #FF6663, #FEB144, #FDFD97, #9EE09E, #9EC1CF, #CC99C9); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; - animation: rainbow 4s ease infinite; - background-size: 400% 400%; - animation-direction: normal; -} - -@keyframes rainbow { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } -} - ::-webkit-scrollbar { width: 8px; margin-left: 2px; diff --git a/src/renderer/css/menu.css b/src/renderer/css/menu.css deleted file mode 100644 index 4f46620d..00000000 --- a/src/renderer/css/menu.css +++ /dev/null @@ -1,64 +0,0 @@ -.waiting { - font-size: 20px; - margin-top: 12px; - margin-bottom: 12px; -} - -.button { - color: white; - background: none; - padding: 2px 10px; - border-radius: 10px; - border: 4px solid white; - font-size: 25px; - outline: none; - font-weight: 500; - font-family: 'Varela', sans-serif; - margin-top: 24px; -} - -.button:hover { - border-color: #00ff00; - cursor: pointer; -} - -.menu { - display: flex; - flex-direction: column; - justify-content: start; - align-items: center; -} - -.errormessage { - margin: 5px; -} - -.title { - width: 100%; - text-align: center; - display: block; - height: 24px; - line-height: 24px; - color: #9b59b6; -} - -.footer { - position: absolute; - bottom: 0; - width: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} - -.footer .row { - width: 100%; - display: flex; - justify-content: space-evenly; - margin: 5px; -} - -.footer svg { - cursor: pointer; -} \ No newline at end of file diff --git a/src/renderer/css/settings.css b/src/renderer/css/settings.css deleted file mode 100644 index 9277ca51..00000000 --- a/src/renderer/css/settings.css +++ /dev/null @@ -1,110 +0,0 @@ -#settings { - transition: transform .2s ease-in; - width: 100vw; - height: 100vh; - background: #171717ad; - backdrop-filter: blur(4px); - position: absolute; - top: 0; - left: 0; - z-index: 10; - display: flex; - flex-direction: column; - justify-content: start; - align-items: center; - padding-top: 20px; -} - -.form-control, h2 { - user-select: none; -} - -.form-control.l { - display: flex; - flex-direction: column; - text-align: center; -} - -.form-control.m { - margin-bottom: 10px; -} - -.titlebar-button.back { - right: 2px; - transition: transform .1s linear; -} - -.titlebar-button.back:hover { - transform: translateX(-1px); -} - -input[type="text"] { - background: #1d1d1d; - border: 1px solid rgba(255, 255, 255, 0.5); - outline: none; - color: white; - padding: 5px; - border-radius: 5px; -} - -.form-control.m .input-error { - border-color: #b00020 -} - -input[type="text"]:focus { - border-color: white; -} - -select { - background: #1d1d1d; - border: 1px solid rgba(255, 255, 255, 0.5); - outline: none; - color: white; - padding: 5px; - border-radius: 5px; - max-width: 240px; -} - -.settings-scroll { - overflow-y: auto; - height: calc(100vh - 50px); - display: flex; - flex-direction: column; - justify-content: start; - align-items: center; - margin-bottom: 30px; - width: 100%; -} - -.microphone-bar { - width: 200px; - height: 10px; - background: #1d1d1d; - border: 1px solid rgba(255, 255, 255, 0.5); - border-radius: 5px; - margin: 5px auto; - overflow: hidden; -} - -.microphone-bar-inner { - background: #e74c3c; - height: 10px; - border-radius: 5px; -} - -.test-speakers { - width: fit-content; - margin: 5px auto; -} - -.settings-alert { - background: #f1c40f; - color: black; - position: absolute; - bottom: 20px; - left: 0; - height: 30px; - justify-content: center; - align-items: center; - width: 100vw; -} \ No newline at end of file From c6c00646d2e5f5561cbb670ca27e6572235c2cf6 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 26 Dec 2020 18:03:02 -0800 Subject: [PATCH 43/88] fix potential quit loop --- src/main/hook.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/hook.ts b/src/main/hook.ts index 2ad0facc..cb0223ff 100644 --- a/src/main/hook.ts +++ b/src/main/hook.ts @@ -134,7 +134,7 @@ type K = keyof typeof keycodeMap; function keyCodeMatches(key: K, ev: IOHookEvent): boolean { if (keycodeMap[key]) return keycodeMap[key] === ev.keycode; - else if (key.length === 1) return key.charCodeAt(0) === ev.rawcode; + else if (key && key.length === 1) return key.charCodeAt(0) === ev.rawcode; else { console.error('Invalid key', key); return false; From 1c685e795159c6e562e0ffc8c8ecae3bce05de34 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 26 Dec 2020 18:08:03 -0800 Subject: [PATCH 44/88] Reset serverURL on 1.2.0 update --- src/renderer/settings/Settings.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/renderer/settings/Settings.tsx b/src/renderer/settings/Settings.tsx index dfd35834..266afc9e 100644 --- a/src/renderer/settings/Settings.tsx +++ b/src/renderer/settings/Settings.tsx @@ -159,6 +159,9 @@ const store = new Store({ store.delete('stereoInLobby'); }, '1.2.0': (store) => { + if (store.get('serverURL') !== 'https://crewl.ink') { + store.set('serverURL', 'https://crewl.ink'); + } // @ts-ignore store.delete('offsets'); }, From 3fb11979acbee9d53b6bebd5bae0172dba5e68c1 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 26 Dec 2020 18:32:46 -0800 Subject: [PATCH 45/88] set user-agent --- src/main/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/index.ts b/src/main/index.ts index 4eac02b1..16e86636 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -42,7 +42,6 @@ function createMainWindow() { }); mainWindowState.manage(window); - if (isDevelopment) { // Force devtools into detached mode otherwise they are unusable window.webContents.openDevTools({ @@ -50,11 +49,14 @@ function createMainWindow() { }); } + let crewlinkVersion: string; if (isDevelopment) { + crewlinkVersion = 'dev'; window.loadURL( `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}?version=DEV` ); } else { + crewlinkVersion = autoUpdater.currentVersion.version; window.loadURL( formatUrl({ pathname: joinPath(__dirname, 'index.html'), @@ -66,6 +68,7 @@ function createMainWindow() { }) ); } + window.webContents.userAgent = `CrewLink/${crewlinkVersion} (${process.platform}) Node/${process.versions.node} Electron/${process.versions.electron}`; window.on('closed', () => { mainWindow = null; From 0ef81482d0633e5e48634c9263f071e275c9fcc5 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 26 Dec 2020 18:37:10 -0800 Subject: [PATCH 46/88] fix database being unfilled --- src/renderer/settings/Settings.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/renderer/settings/Settings.tsx b/src/renderer/settings/Settings.tsx index 266afc9e..8107b82a 100644 --- a/src/renderer/settings/Settings.tsx +++ b/src/renderer/settings/Settings.tsx @@ -216,6 +216,9 @@ const store = new Store({ default: 5.32, }, }, + default: { + maxDistance: 5.32 + } }, }, }); @@ -309,7 +312,6 @@ const URLInput: React.FC = function ({ let url = event.target.value.trim(); if (url.endsWith('/')) url = url.substring(0, url.length - 1); setCurrentURL(url); - console.log(url, validateServerUrl(url)); if (validateServerUrl(url)) { setURLValid(true); } else { @@ -392,7 +394,6 @@ const Settings: React.FC = function ({ type: 'set', action: store.store, }); - console.log(store.get('localLobbySettings')); setLobbySettings({ type: 'set', action: store.get('localLobbySettings') From 7e1a25cf4632fb4a130691cdc35e12d3358aff85 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 26 Dec 2020 18:46:04 -0800 Subject: [PATCH 47/88] change up url parsing --- src/renderer/settings/Settings.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/renderer/settings/Settings.tsx b/src/renderer/settings/Settings.tsx index 8107b82a..91615de5 100644 --- a/src/renderer/settings/Settings.tsx +++ b/src/renderer/settings/Settings.tsx @@ -278,7 +278,6 @@ interface MediaDevice { function validateServerUrl(uri: string): boolean { try { - if (uri.endsWith('/')) return false; if (!isHttpUri(uri) && !isHttpsUri(uri)) return false; const url = new URL(uri); if (url.hostname === 'discord.gg') return false; @@ -310,7 +309,6 @@ const URLInput: React.FC = function ({ function handleChange(event: React.ChangeEvent) { let url = event.target.value.trim(); - if (url.endsWith('/')) url = url.substring(0, url.length - 1); setCurrentURL(url); if (validateServerUrl(url)) { setURLValid(true); @@ -336,7 +334,7 @@ const URLInput: React.FC = function ({ color="primary" helperText={isValidURL ? '' : 'Invalid URL'} /> - This option is for advanced users only. Untrusted servers can potentially steal your info or crash CrewLink. + This option is for advanced users only. Other servers can steal your info or crash CrewLink.
From 64c311d901abf707914692bcd9838f0bcb873c62 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 26 Dec 2020 18:46:13 -0800 Subject: [PATCH 48/88] shorten user-agent --- src/main/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/index.ts b/src/main/index.ts index 16e86636..6e438fd1 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -68,7 +68,7 @@ function createMainWindow() { }) ); } - window.webContents.userAgent = `CrewLink/${crewlinkVersion} (${process.platform}) Node/${process.versions.node} Electron/${process.versions.electron}`; + window.webContents.userAgent = `CrewLink/${crewlinkVersion} (${process.platform})`; window.on('closed', () => { mainWindow = null; From 01578a527f7ac8d57b962d7b7e7905ede557841b Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 26 Dec 2020 18:46:48 -0800 Subject: [PATCH 49/88] dev => 0.0.0 --- src/main/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/index.ts b/src/main/index.ts index 6e438fd1..aff7a459 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -51,7 +51,7 @@ function createMainWindow() { let crewlinkVersion: string; if (isDevelopment) { - crewlinkVersion = 'dev'; + crewlinkVersion = '0.0.0'; window.loadURL( `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}?version=DEV` ); From 08595c97d01f9a5977187da81db0d64380ecad8f Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 26 Dec 2020 19:19:08 -0800 Subject: [PATCH 50/88] Accept server errors --- src/renderer/Voice.tsx | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index 6d425eb8..de10b304 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -65,6 +65,10 @@ interface Client { clientId: number; } +interface SocketError { + message?: string; +} + function calculateVoiceAudio( state: AmongUsState, settings: ISettings, @@ -183,7 +187,8 @@ const useStyles = makeStyles((theme) => ({ }, })); -const Voice: React.FC = function ({ error }: VoiceProps) { +const Voice: React.FC = function ({ error: initialError }: VoiceProps) { + const [error, setError] = useState(initialError); const [settings, setSettings] = useContext(SettingsContext); const settingsRef = useRef(settings); const [lobbySettings, setLobbySettings] = useContext(LobbySettingsContext); @@ -295,6 +300,11 @@ const Voice: React.FC = function ({ error }: VoiceProps) { }); const { socket } = connectionStuff.current; + socket.on('error', (error: SocketError) => { + if (error.message) { + setError(error.message); + } + }); socket.on('connect', () => { setConnected(true); }); @@ -500,10 +510,11 @@ const Voice: React.FC = function ({ error }: VoiceProps) { }, (error) => { console.error(error); - ipcRenderer.send(IpcMessages.SHOW_ERROR_DIALOG, { - title: 'Error', - content: 'Couldn\'t connect to your microphone:\n' + error - }); + setError('Couldn\'t connect to your microphone:\n' + error); + // ipcRenderer.send(IpcMessages.SHOW_ERROR_DIALOG, { + // title: 'Error', + // content: 'Couldn\'t connect to your microphone:\n' + error + // }); } ); From 5c9dcecc4f0b2da34c1f099870ef44256f6f5d7f Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 26 Dec 2020 19:19:33 -0800 Subject: [PATCH 51/88] unused variable --- src/renderer/Voice.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index de10b304..bcb1523e 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -11,7 +11,7 @@ import Peer from 'simple-peer'; import { ipcRenderer } from 'electron'; import VAD from './vad'; import { ISettings } from '../common/ISettings'; -import { IpcMessages, IpcRendererMessages } from '../common/ipc-messages'; +import { IpcRendererMessages } from '../common/ipc-messages'; import Typography from '@material-ui/core/Typography'; import Grid from '@material-ui/core/Grid'; import makeStyles from '@material-ui/core/styles/makeStyles'; From 619a4be65adb6de36fc6cdf94930ad4eecd9be2b Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 26 Dec 2020 19:21:14 -0800 Subject: [PATCH 52/88] lint --- src/common/ipc-messages.ts | 34 ++--- src/main/hook.ts | 12 +- src/main/index.ts | 12 +- src/main/ipc-handlers.ts | 41 +++--- src/renderer/App.tsx | 102 ++++++++++----- src/renderer/Avatar.tsx | 7 +- src/renderer/Menu.tsx | 10 +- src/renderer/Voice.tsx | 69 ++++++---- src/renderer/settings/MicrophoneSoundBar.tsx | 5 +- src/renderer/settings/Settings.tsx | 127 +++++++++++++------ src/renderer/theme.ts | 4 +- 11 files changed, 278 insertions(+), 145 deletions(-) diff --git a/src/common/ipc-messages.ts b/src/common/ipc-messages.ts index aae0dd9d..9c152e19 100644 --- a/src/common/ipc-messages.ts +++ b/src/common/ipc-messages.ts @@ -2,35 +2,35 @@ import { ProgressInfo } from 'builder-util-runtime'; // Renderer --> Main (send/on) export enum IpcMessages { - SHOW_ERROR_DIALOG = 'SHOW_ERROR_DIALOG', - OPEN_AMONG_US_GAME = 'OPEN_AMONG_US_GAME', - RESTART_CREWLINK = 'RESTART_CREWLINK', - QUIT_CREWLINK = 'QUIT_CREWLINK', + SHOW_ERROR_DIALOG = 'SHOW_ERROR_DIALOG', + OPEN_AMONG_US_GAME = 'OPEN_AMONG_US_GAME', + RESTART_CREWLINK = 'RESTART_CREWLINK', + QUIT_CREWLINK = 'QUIT_CREWLINK', } // Renderer --> Main (sendSync/on) export enum IpcSyncMessages { - GET_INITIAL_STATE = 'GET_INITIAL_STATE', + GET_INITIAL_STATE = 'GET_INITIAL_STATE', } // Renderer --> Main (invoke/handle) export enum IpcHandlerMessages { - START_HOOK = 'START_HOOK', + START_HOOK = 'START_HOOK', } // Main --> Renderer (send/on) export enum IpcRendererMessages { - NOTIFY_GAME_OPENED = 'NOTIFY_GAME_OPENED', - NOTIFY_GAME_STATE_CHANGED = 'NOTIFY_GAME_STATE_CHANGED', - TOGGLE_DEAFEN = 'TOGGLE_DEAFEN', - TOGGLE_MUTE = 'TOGGLE_MUTE', - PUSH_TO_TALK = 'PUSH_TO_TALK', - ERROR = 'ERROR', - AUTO_UPDATER_STATE = 'AUTO_UPDATER_STATE', + NOTIFY_GAME_OPENED = 'NOTIFY_GAME_OPENED', + NOTIFY_GAME_STATE_CHANGED = 'NOTIFY_GAME_STATE_CHANGED', + TOGGLE_DEAFEN = 'TOGGLE_DEAFEN', + TOGGLE_MUTE = 'TOGGLE_MUTE', + PUSH_TO_TALK = 'PUSH_TO_TALK', + ERROR = 'ERROR', + AUTO_UPDATER_STATE = 'AUTO_UPDATER_STATE', } export interface AutoUpdaterState { - state: 'error' | 'available' | 'downloading' | 'downloaded' | 'unavailable'; - error?: string; - progress?: ProgressInfo; -} \ No newline at end of file + state: 'error' | 'available' | 'downloading' | 'downloaded' | 'unavailable'; + error?: string; + progress?: ProgressInfo; +} diff --git a/src/main/hook.ts b/src/main/hook.ts index cb0223ff..a853ec31 100644 --- a/src/main/hook.ts +++ b/src/main/hook.ts @@ -3,7 +3,11 @@ import GameReader from './GameReader'; import iohook from 'iohook'; import Store from 'electron-store'; import { ISettings } from '../common/ISettings'; -import { IpcHandlerMessages, IpcRendererMessages, IpcSyncMessages } from '../common/ipc-messages'; +import { + IpcHandlerMessages, + IpcRendererMessages, + IpcSyncMessages, +} from '../common/ipc-messages'; interface IOHookEvent { type: string; @@ -23,7 +27,9 @@ let gameReader: GameReader; ipcMain.on(IpcSyncMessages.GET_INITIAL_STATE, (event) => { if (!readingGame) { - console.error('Recieved GET_INITIAL_STATE message before the START_HOOK message was received'); + console.error( + 'Recieved GET_INITIAL_STATE message before the START_HOOK message was received' + ); event.returnValue = null; return; } @@ -157,4 +163,4 @@ function mouseClickMatches(key: M, ev: IOHookEvent): boolean { function isMouseButton(shortcutKey: string): boolean { return shortcutKey.includes('MouseButton'); -} \ No newline at end of file +} diff --git a/src/main/index.ts b/src/main/index.ts index aff7a459..0a3a9c8d 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -37,15 +37,15 @@ function createMainWindow() { transparent: true, webPreferences: { nodeIntegration: true, - webSecurity: false - } + webSecurity: false, + }, }); mainWindowState.manage(window); if (isDevelopment) { // Force devtools into detached mode otherwise they are unusable window.webContents.openDevTools({ - mode: 'detach' + mode: 'detach', }); } @@ -91,19 +91,19 @@ if (!gotTheLock) { autoUpdater.checkForUpdates(); autoUpdater.on('update-available', () => { mainWindow?.webContents.send(IpcRendererMessages.AUTO_UPDATER_STATE, { - state: 'available' + state: 'available', }); }); autoUpdater.on('error', (err: string) => { mainWindow?.webContents.send(IpcRendererMessages.AUTO_UPDATER_STATE, { state: 'error', - error: err + error: err, }); }); autoUpdater.on('download-progress', (progress: ProgressInfo) => { mainWindow?.webContents.send(IpcRendererMessages.AUTO_UPDATER_STATE, { state: 'downloading', - progress + progress, }); }); autoUpdater.on('update-downloaded', () => { diff --git a/src/main/ipc-handlers.ts b/src/main/ipc-handlers.ts index ce8a7edb..51d2dde7 100644 --- a/src/main/ipc-handlers.ts +++ b/src/main/ipc-handlers.ts @@ -6,27 +6,36 @@ import path from 'path'; import { IpcMessages } from '../common/ipc-messages'; // Listeners are fire and forget, they do not have "responses" or return values -export const initializeIpcListeners = () => { - ipcMain.on(IpcMessages.SHOW_ERROR_DIALOG, (e, opts: { title: string; content: string; }) => { - if (typeof opts === 'object' && opts && typeof opts.title === 'string' && typeof opts.content === 'string') { - dialog.showErrorBox(opts.title, opts.content); +export const initializeIpcListeners = (): void => { + ipcMain.on( + IpcMessages.SHOW_ERROR_DIALOG, + (e, opts: { title: string; content: string }) => { + if ( + typeof opts === 'object' && + opts && + typeof opts.title === 'string' && + typeof opts.content === 'string' + ) { + dialog.showErrorBox(opts.title, opts.content); + } } - }); + ); ipcMain.on(IpcMessages.OPEN_AMONG_US_GAME, () => { // Get steam path from registry - const steamPath = enumerateValues(HKEY.HKEY_LOCAL_MACHINE, - 'SOFTWARE\\WOW6432Node\\Valve\\Steam') - .find(v => v.name === 'InstallPath'); + const steamPath = enumerateValues( + HKEY.HKEY_LOCAL_MACHINE, + 'SOFTWARE\\WOW6432Node\\Valve\\Steam' + ).find((v) => v.name === 'InstallPath'); // Check if Steam is installed if (!steamPath) { dialog.showErrorBox('Error', 'Could not find your Steam install path.'); } else { try { - const process = spawn(path.join(steamPath.data as string, 'steam.exe'), [ - '-applaunch', - '945360' - ]); + const process = spawn( + path.join(steamPath.data as string, 'steam.exe'), + ['-applaunch', '945360'] + ); process.on('error', () => { dialog.showErrorBox('Error', 'Please launch the game through Steam.'); }); @@ -35,12 +44,12 @@ export const initializeIpcListeners = () => { } } }); - + ipcMain.on(IpcMessages.RESTART_CREWLINK, () => { app.relaunch(); app.quit(); }); - + ipcMain.on(IpcMessages.QUIT_CREWLINK, () => { for (const win of BrowserWindow.getAllWindows()) { win.close(); @@ -52,6 +61,6 @@ export const initializeIpcListeners = () => { // Handlers are async cross-process instructions, they should have a return value // or the caller should be "await"'ing them. If neither of these are the case // consider making it a "listener" instead for performance and readability -export const initializeIpcHandlers = () => { +export const initializeIpcHandlers = (): void => { // TODO: Put handlers here -}; \ No newline at end of file +}; diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 5ec45be5..587e96de 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -20,7 +20,13 @@ import { LobbySettingsContext, } from './contexts'; import { ThemeProvider } from '@material-ui/core/styles'; -import { AutoUpdaterState, IpcHandlerMessages, IpcMessages, IpcRendererMessages, IpcSyncMessages } from '../common/ipc-messages'; +import { + AutoUpdaterState, + IpcHandlerMessages, + IpcMessages, + IpcRendererMessages, + IpcSyncMessages, +} from '../common/ipc-messages'; import theme from './theme'; import SettingsIcon from '@material-ui/icons/Settings'; import CloseIcon from '@material-ui/icons/Close'; @@ -110,7 +116,9 @@ function App() { const [gameState, setGameState] = useState({} as AmongUsState); const [settingsOpen, setSettingsOpen] = useState(false); const [error, setError] = useState(''); - const [updaterState, setUpdaterState] = useState({ state: 'unavailable' }); + const [updaterState, setUpdaterState] = useState({ + state: 'unavailable', + }); const settings = useReducer(settingsReducer, { alwaysOnTop: false, microphone: 'Default', @@ -126,7 +134,10 @@ function App() { maxDistance: 5.32, }, }); - const lobbySettings = useReducer(lobbySettingsReducer, settings[0].localLobbySettings); + const lobbySettings = useReducer( + lobbySettingsReducer, + settings[0].localLobbySettings + ); useEffect(() => { const onOpen = (_: Electron.IpcRendererEvent, isOpen: boolean) => { @@ -139,26 +150,38 @@ function App() { shouldInit = false; setError(error); }; - const onAutoUpdaterStateChange = (_: Electron.IpcRendererEvent, state: AutoUpdaterState) => { - setUpdaterState(old => ({...old, ...state})); - } + const onAutoUpdaterStateChange = ( + _: Electron.IpcRendererEvent, + state: AutoUpdaterState + ) => { + setUpdaterState((old) => ({ ...old, ...state })); + }; let shouldInit = true; - ipcRenderer.invoke(IpcHandlerMessages.START_HOOK).then(() => { - if (shouldInit) { - setGameState(ipcRenderer.sendSync(IpcSyncMessages.GET_INITIAL_STATE)); - } - }).catch((error: Error) => { - if (shouldInit) { - shouldInit = false; - setError(error.message); - } - }); - ipcRenderer.on(IpcRendererMessages.AUTO_UPDATER_STATE, onAutoUpdaterStateChange); + ipcRenderer + .invoke(IpcHandlerMessages.START_HOOK) + .then(() => { + if (shouldInit) { + setGameState(ipcRenderer.sendSync(IpcSyncMessages.GET_INITIAL_STATE)); + } + }) + .catch((error: Error) => { + if (shouldInit) { + shouldInit = false; + setError(error.message); + } + }); + ipcRenderer.on( + IpcRendererMessages.AUTO_UPDATER_STATE, + onAutoUpdaterStateChange + ); ipcRenderer.on(IpcRendererMessages.NOTIFY_GAME_OPENED, onOpen); ipcRenderer.on(IpcRendererMessages.NOTIFY_GAME_STATE_CHANGED, onState); ipcRenderer.on(IpcRendererMessages.ERROR, onError); return () => { - ipcRenderer.off(IpcRendererMessages.AUTO_UPDATER_STATE, onAutoUpdaterStateChange); + ipcRenderer.off( + IpcRendererMessages.AUTO_UPDATER_STATE, + onAutoUpdaterStateChange + ); ipcRenderer.off(IpcRendererMessages.NOTIFY_GAME_OPENED, onOpen); ipcRenderer.off(IpcRendererMessages.NOTIFY_GAME_STATE_CHANGED, onState); ipcRenderer.off(IpcRendererMessages.ERROR, onError); @@ -192,20 +215,37 @@ function App() { Updating... - {((updaterState.state === 'downloading' || updaterState.state === 'downloaded') && updaterState.progress) && - <> - - {prettyBytes(updaterState.progress.transferred)} / {prettyBytes(updaterState.progress.total)} - - } - { - updaterState.state === 'error' && - {updaterState.error} - } + {(updaterState.state === 'downloading' || + updaterState.state === 'downloaded') && + updaterState.progress && ( + <> + + + {prettyBytes(updaterState.progress.transferred)} /{' '} + {prettyBytes(updaterState.progress.total)} + + + )} + {updaterState.state === 'error' && ( + + {updaterState.error} + + )} - {updaterState.state === 'error' && - - } + {updaterState.state === 'error' && ( + + + + )} {page} diff --git a/src/renderer/Avatar.tsx b/src/renderer/Avatar.tsx index f8b052ea..07aff0c4 100644 --- a/src/renderer/Avatar.tsx +++ b/src/renderer/Avatar.tsx @@ -89,7 +89,12 @@ const Avatar: React.FC = function ({ } break; case 'novoice': - icon = ; + icon = ( + + ); break; case 'disconnected': icon = ; diff --git a/src/renderer/Menu.tsx b/src/renderer/Menu.tsx index 3a7d3958..a2833323 100644 --- a/src/renderer/Menu.tsx +++ b/src/renderer/Menu.tsx @@ -20,12 +20,12 @@ const useStyles = makeStyles((theme) => ({ display: 'flex', flexDirection: 'column', alignItems: 'center', - justifyContent: 'start' + justifyContent: 'start', }, waiting: { fontSize: 20, marginTop: 12, - marginBottom: 12 + marginBottom: 12, }, button: { color: 'white', @@ -40,9 +40,9 @@ const useStyles = makeStyles((theme) => ({ marginTop: 24, '&:hover': { borderColor: '#00ff00', - cursor: 'pointer' - } - } + cursor: 'pointer', + }, + }, })); export interface MenuProps { diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index bcb1523e..1f9f6043 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -26,7 +26,17 @@ interface PeerConnections { [peer: string]: Peer.Instance; } -type PeerErrorCode = 'ERR_WEBRTC_SUPPORT' | 'ERR_CREATE_OFFER' | 'ERR_CREATE_ANSWER' | 'ERR_SET_LOCAL_DESCRIPTION' | 'ERR_SET_REMOTE_DESCRIPTION' | 'ERR_ADD_ICE_CANDIDATE' | 'ERR_ICE_CONNECTION_FAILURE' | 'ERR_SIGNALING' | 'ERR_DATA_CHANNEL' | 'ERR_CONNECTION_FAILURE'; +type PeerErrorCode = + | 'ERR_WEBRTC_SUPPORT' + | 'ERR_CREATE_OFFER' + | 'ERR_CREATE_ANSWER' + | 'ERR_SET_LOCAL_DESCRIPTION' + | 'ERR_SET_REMOTE_DESCRIPTION' + | 'ERR_ADD_ICE_CANDIDATE' + | 'ERR_ICE_CONNECTION_FAILURE' + | 'ERR_SIGNALING' + | 'ERR_DATA_CHANNEL' + | 'ERR_CONNECTION_FAILURE'; interface AudioElements { [peer: string]: { @@ -187,7 +197,9 @@ const useStyles = makeStyles((theme) => ({ }, })); -const Voice: React.FC = function ({ error: initialError }: VoiceProps) { +const Voice: React.FC = function ({ + error: initialError, +}: VoiceProps) { const [error, setError] = useState(initialError); const [settings, setSettings] = useContext(SettingsContext); const settingsRef = useRef(settings); @@ -244,7 +256,7 @@ const Voice: React.FC = function ({ error: initialError }: VoiceProp try { peer.send(JSON.stringify(settings.localLobbySettings)); } catch (e) { - console.warn("failed to update lobby settings: ", e); + console.warn('failed to update lobby settings: ', e); } }); }, [settings.localLobbySettings]); @@ -351,12 +363,15 @@ const Voice: React.FC = function ({ error: initialError }: VoiceProp setMuted(connectionStuff.current.muted); setDeafened(connectionStuff.current.deafened); }); - ipcRenderer.on(IpcRendererMessages.PUSH_TO_TALK, (_: unknown, pressing: boolean) => { - if (!connectionStuff.current.pushToTalk) return; - if (!connectionStuff.current.deafened) { - stream.getAudioTracks()[0].enabled = pressing; + ipcRenderer.on( + IpcRendererMessages.PUSH_TO_TALK, + (_: unknown, pressing: boolean) => { + if (!connectionStuff.current.pushToTalk) return; + if (!connectionStuff.current.deafened) { + stream.getAudioTracks()[0].enabled = pressing; + } } - }); + ); const ac = new AudioContext(); ac.createMediaStreamSource(stream); @@ -412,12 +427,12 @@ const Voice: React.FC = function ({ error: initialError }: VoiceProp try { connection.send(JSON.stringify(lobbySettingsRef.current)); } catch (e) { - console.warn("failed to update lobby settings: ", e); + console.warn('failed to update lobby settings: ', e); } } }); connection.on('stream', (stream: MediaStream) => { - setAudioConnected(old => ({ ...old, [peer]: true })); + setAudioConnected((old) => ({ ...old, [peer]: true })); const audio = document.createElement( 'audio' ) as ExtendedAudioElement; @@ -478,7 +493,13 @@ const Voice: React.FC = function ({ error: initialError }: VoiceProp disconnectPeer(peer); // Auto reconnect on connection error - if (initiator && errCode && retries < 10 && (errCode == 'ERR_CONNECTION_FAILURE' || errCode == 'ERR_DATA_CHANNEL')) { + if ( + initiator && + errCode && + retries < 10 && + (errCode == 'ERR_CONNECTION_FAILURE' || + errCode == 'ERR_DATA_CHANNEL') + ) { createPeerConnection(peer, initiator); retries++; } @@ -510,7 +531,7 @@ const Voice: React.FC = function ({ error: initialError }: VoiceProp }, (error) => { console.error(error); - setError('Couldn\'t connect to your microphone:\n' + error); + setError("Couldn't connect to your microphone:\n" + error); // ipcRenderer.send(IpcMessages.SHOW_ERROR_DIALOG, { // title: 'Error', // content: 'Couldn\'t connect to your microphone:\n' + error @@ -591,8 +612,11 @@ const Voice: React.FC = function ({ error: initialError }: VoiceProp gameState.oldGameState === GameState.TASKS) ) { connect.connect(gameState.lobbyCode, myPlayer.id, gameState.clientId); - } - else if (gameState.oldGameState !== GameState.UNKNOWN && gameState.oldGameState !== GameState.MENU && gameState.gameState === GameState.MENU) { + } else if ( + gameState.oldGameState !== GameState.UNKNOWN && + gameState.oldGameState !== GameState.MENU && + gameState.gameState === GameState.MENU + ) { // On change from a game to menu, exit from the current game properly connectionStuff.current.socket?.emit('leave'); Object.keys(peerConnections).forEach((k) => { @@ -600,7 +624,6 @@ const Voice: React.FC = function ({ error: initialError }: VoiceProp }); setOtherDead({}); } - }, [gameState.gameState]); useEffect(() => { @@ -628,7 +651,7 @@ const Voice: React.FC = function ({ error: initialError }: VoiceProp }, [myPlayer?.id]); const playerSocketIds: { - [index: number]: string + [index: number]: string; } = {}; for (const k of Object.keys(socketClients)) { @@ -665,9 +688,7 @@ const Voice: React.FC = function ({ error: initialError }: VoiceProp )}
{myPlayer && gameState?.gameState !== GameState.MENU && ( - - {myPlayer.name} - + {myPlayer.name} )} {gameState.lobbyCode && ( = function ({ error: initialError }: VoiceProp xs={getPlayersPerRow(otherPlayers.length)} > ); })} - -
+ +
); }; diff --git a/src/renderer/settings/MicrophoneSoundBar.tsx b/src/renderer/settings/MicrophoneSoundBar.tsx index ac7e0eae..6d7bdedc 100644 --- a/src/renderer/settings/MicrophoneSoundBar.tsx +++ b/src/renderer/settings/MicrophoneSoundBar.tsx @@ -56,7 +56,10 @@ const TestMicrophoneButton: React.FC = function ({ }; navigator.mediaDevices - .getUserMedia({ audio: { deviceId: microphone ?? 'default' }, video: false }) + .getUserMedia({ + audio: { deviceId: microphone ?? 'default' }, + video: false, + }) .then((stream) => { const src = ctx.createMediaStreamSource(stream); src.connect(processor); diff --git a/src/renderer/settings/Settings.tsx b/src/renderer/settings/Settings.tsx index 91615de5..c338ab3d 100644 --- a/src/renderer/settings/Settings.tsx +++ b/src/renderer/settings/Settings.tsx @@ -1,5 +1,11 @@ import Store from 'electron-store'; -import React, { ReactChild, useContext, useEffect, useReducer, useState } from 'react'; +import React, { + ReactChild, + useContext, + useEffect, + useReducer, + useState, +} from 'react'; import { SettingsContext, LobbySettingsContext, @@ -99,9 +105,9 @@ const useStyles = makeStyles((theme) => ({ alignItems: 'center', justifyContent: 'start', '&>*': { - marginBottom: theme.spacing(1) - } - } + marginBottom: theme.spacing(1), + }, + }, })); const keys = new Set([ @@ -217,8 +223,8 @@ const store = new Store({ }, }, default: { - maxDistance: 5.32 - } + maxDistance: 5.32, + }, }, }, }); @@ -275,7 +281,6 @@ interface MediaDevice { label: string; } - function validateServerUrl(uri: string): boolean { try { if (!isHttpUri(uri) && !isHttpsUri(uri)) return false; @@ -297,7 +302,7 @@ type URLInputProps = { const URLInput: React.FC = function ({ initialURL, onValidURL, - className + className, }: URLInputProps) { const [isValidURL, setURLValid] = useState(true); const [currentURL, setCurrentURL] = useState(initialURL); @@ -308,7 +313,7 @@ const URLInput: React.FC = function ({ }, [initialURL]); function handleChange(event: React.ChangeEvent) { - let url = event.target.value.trim(); + const url = event.target.value.trim(); setCurrentURL(url); if (validateServerUrl(url)) { setURLValid(true); @@ -319,7 +324,13 @@ const URLInput: React.FC = function ({ return ( <> - + setOpen(false)}> Change Voice Server @@ -334,25 +345,45 @@ const URLInput: React.FC = function ({ color="primary" helperText={isValidURL ? '' : 'Invalid URL'} /> - This option is for advanced users only. Other servers can steal your info or crash CrewLink. - + + This option is for advanced users only. Other servers can steal your + info or crash CrewLink. + + - - + + @@ -365,19 +396,19 @@ interface DisabledTooltipProps { children: ReactChild; } -const DisabledTooltip: React.FC = function ({ disabled, children, title }: DisabledTooltipProps) { +const DisabledTooltip: React.FC = function ({ + disabled, + children, + title, +}: DisabledTooltipProps) { if (disabled) return ( {children} ); - else return ( - <> - {children} - - ); -} + else return <>{children}; +}; const Settings: React.FC = function ({ open, @@ -396,7 +427,7 @@ const Settings: React.FC = function ({ }); setLobbySettings({ type: 'set', - action: store.get('localLobbySettings') + action: store.get('localLobbySettings'), }); }, []); @@ -466,13 +497,19 @@ const Settings: React.FC = function ({ const microphones = devices.filter((d) => d.kind === 'audioinput'); const speakers = devices.filter((d) => d.kind === 'audiooutput'); - const [localDistance, setLocalDistance] = useState(settings.localLobbySettings.maxDistance); + const [localDistance, setLocalDistance] = useState( + settings.localLobbySettings.maxDistance + ); useEffect(() => { setLocalDistance(settings.localLobbySettings.maxDistance); }, [settings.localLobbySettings.maxDistance]); - const isInMenuOrLobby = gameState?.gameState === GameState.LOBBY || gameState?.gameState === GameState.MENU; - const canChangeLobbySettings = (gameState?.gameState === GameState.MENU) || (gameState?.isHost && gameState?.gameState === GameState.LOBBY); + const isInMenuOrLobby = + gameState?.gameState === GameState.LOBBY || + gameState?.gameState === GameState.MENU; + const canChangeLobbySettings = + gameState?.gameState === GameState.MENU || + (gameState?.isHost && gameState?.gameState === GameState.LOBBY); return ( @@ -499,8 +536,18 @@ const Settings: React.FC = function ({ {/* Lobby Settings */}
Lobby Settings - Voice Distance: {canChangeLobbySettings ? localDistance : lobbySettings.maxDistance} - + + Voice Distance:{' '} + {canChangeLobbySettings ? localDistance : lobbySettings.maxDistance} + + Date: Sat, 26 Dec 2020 19:21:30 -0800 Subject: [PATCH 53/88] v1.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ec6ad38c..f7400593 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "crewlink", - "version": "1.1.6", + "version": "1.2.0", "license": "GPL-3.0-or-later", "description": "Free, open, Among Us proximity voice chat", "repository": { From 7b871e59447eed22c6a6f6d7b538b2d9e1e242d9 Mon Sep 17 00:00:00 2001 From: OhMyGuus Date: Sun, 27 Dec 2020 20:04:59 +0100 Subject: [PATCH 54/88] Forgot to commit change. --- src/main/offsetStore.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/offsetStore.ts b/src/main/offsetStore.ts index 85083360..c7c4b7f4 100644 --- a/src/main/offsetStore.ts +++ b/src/main/offsetStore.ts @@ -1,4 +1,4 @@ -export interface IOffsetsContainer { +export interface IOffsetsStore { x64: IOffsets; x86: IOffsets; } @@ -182,4 +182,4 @@ export default { }, }, }, -} as IOffsetsContainer; +} as IOffsetsStore; From a5a523577e0dc581f4e8c3be0903e7be1a1071e4 Mon Sep 17 00:00:00 2001 From: OhMyGuus Date: Sun, 27 Dec 2020 20:08:15 +0100 Subject: [PATCH 55/88] lint --- src/main/GameReader.ts | 7 ++++++- src/main/memoryjs.d.ts | 9 ++++++++- src/main/offsetStore.ts | 6 +++--- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/main/GameReader.ts b/src/main/GameReader.ts index 0e2338bc..322c4dfb 100644 --- a/src/main/GameReader.ts +++ b/src/main/GameReader.ts @@ -319,7 +319,12 @@ export default class GameReader { ): T { if (!this.amongUs) return defaultParam as T; if (address === 0) return defaultParam as T; - dataType = (dataType == 'pointer' || dataType == 'ptr')? (this.is_64bit? 'uint64' : 'uint32') : dataType; + dataType = + dataType == 'pointer' || dataType == 'ptr' + ? this.is_64bit + ? 'uint64' + : 'uint32' + : dataType; const { address: addr, last } = this.offsetAddress(address, offsets); if (addr === 0) return defaultParam as T; return readMemoryRaw(this.amongUs.handle, addr + last, dataType); diff --git a/src/main/memoryjs.d.ts b/src/main/memoryjs.d.ts index f9929494..2ad5f948 100644 --- a/src/main/memoryjs.d.ts +++ b/src/main/memoryjs.d.ts @@ -97,7 +97,14 @@ declare module 'memoryjs' { buffer: Buffer ): void; - export function findPattern(handle: number, moduleName: string, signature: string, signatureType: number , patternOffset: number, addressOffset: number): number; + export function findPattern( + handle: number, + moduleName: string, + signature: string, + signatureType: number, + patternOffset: number, + addressOffset: number + ): number; // Functions diff --git a/src/main/offsetStore.ts b/src/main/offsetStore.ts index c7c4b7f4..41c96e12 100644 --- a/src/main/offsetStore.ts +++ b/src/main/offsetStore.ts @@ -50,9 +50,9 @@ export interface IOffsets { }[]; }; signatures: { - innerNetClient: ISignature - meetingHud: ISignature - gameData: ISignature + innerNetClient: ISignature; + meetingHud: ISignature; + gameData: ISignature; }; } From bafb16a562a5a0fc0c542a4736c4e0b577732484 Mon Sep 17 00:00:00 2001 From: OhMyGuus Date: Sun, 27 Dec 2020 20:45:41 +0100 Subject: [PATCH 56/88] Error handling for loop function --- src/main/GameReader.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/GameReader.ts b/src/main/GameReader.ts index 322c4dfb..255d99ba 100644 --- a/src/main/GameReader.ts +++ b/src/main/GameReader.ts @@ -77,8 +77,13 @@ export default class GameReader { return; } - loop(): void { - this.checkProcessOpen(); + loop(): string | null { + + try { + this.checkProcessOpen(); + } catch (e) { + return e; + } if ( this.PlayerStruct && this.offsets && @@ -246,6 +251,7 @@ export default class GameReader { this.lastState = newState; this.oldGameState = state; } + return null; } constructor(sendIPC: Electron.WebContents['send']) { From d6931a61189e0bbd3844d5e09851dcf57b8dfc06 Mon Sep 17 00:00:00 2001 From: fabiryn Date: Sun, 27 Dec 2020 22:37:43 +0100 Subject: [PATCH 57/88] fix novoice icon for host --- src/renderer/Voice.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index 1f9f6043..1c04b1ed 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -655,7 +655,7 @@ const Voice: React.FC = function ({ } = {}; for (const k of Object.keys(socketClients)) { - if (socketClients[k].playerId) + if (socketClients[k].playerId !== undefined) playerSocketIds[socketClients[k].playerId] = k; } return ( From 8f65fa1cf29f86b1949a0b825c6656db451ee68f Mon Sep 17 00:00:00 2001 From: fabiryn Date: Sun, 27 Dec 2020 22:39:24 +0100 Subject: [PATCH 58/88] fix skin size for crewmates --- src/renderer/Avatar.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/renderer/Avatar.tsx b/src/renderer/Avatar.tsx index 07aff0c4..ca55e6f5 100644 --- a/src/renderer/Avatar.tsx +++ b/src/renderer/Avatar.tsx @@ -141,6 +141,7 @@ const useCanvasStyles = makeStyles(() => ({ position: 'absolute', top: '38%', left: '17%', + width: '73.5%', transform: 'scale(0.8)', zIndex: 3, display: ({ isAlive }: UseCanvasStylesParams) => From 919d06d69389bf224123f4ea32cee168d678410e Mon Sep 17 00:00:00 2001 From: fabiryn Date: Sun, 27 Dec 2020 22:53:58 +0100 Subject: [PATCH 59/88] fix join issue when host left lobby before creating own one --- src/main/GameReader.ts | 3 ++- src/renderer/Voice.tsx | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/GameReader.ts b/src/main/GameReader.ts index a6ffc1d8..9a361880 100644 --- a/src/main/GameReader.ts +++ b/src/main/GameReader.ts @@ -202,7 +202,8 @@ export default class GameReader { this.PlayerStruct ); playerAddrPtr += 4; - players.push(player); + if (state !== GameState.MENU) + players.push(player); if ( player.name === '' || diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index 1c04b1ed..d830c868 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -390,7 +390,6 @@ const Voice: React.FC = function ({ clientId: number ) => { console.log('Connect called', lobbyCode, playerId, clientId); - socket.emit('leave'); if (lobbyCode === 'MENU') { Object.keys(peerConnections).forEach((k) => { disconnectPeer(k); @@ -476,7 +475,7 @@ const Voice: React.FC = function ({ }); }); connection.on('data', (data) => { - if (gameState.hostId !== socketClientsRef.current[peer].clientId) + if (gameState.hostId !== socketClientsRef.current[peer]?.clientId) return; const settings = JSON.parse(data); Object.keys(lobbySettings).forEach((field: string) => { From 9d2cc566f495be405b78b04746cd19ea189ff35c Mon Sep 17 00:00:00 2001 From: OhMyGuus Date: Mon, 28 Dec 2020 20:16:14 +0100 Subject: [PATCH 60/88] - Forgot to add clientid - Changed method for islocal (wont break when someone dc's) -- This branch is now finished for merge --- src/main/GameReader.ts | 50 +++++++++++++++++++++-------------------- src/main/offsetStore.ts | 4 +--- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/main/GameReader.ts b/src/main/GameReader.ts index 322c4dfb..2197cbfe 100644 --- a/src/main/GameReader.ts +++ b/src/main/GameReader.ts @@ -136,6 +136,18 @@ export default class GameReader { this.offsets.gameCode ) ); + + const hostId = this.readMemory( + 'uint32', + this.gameAssembly.modBaseAddr, + this.offsets.hostId + ); + const clientId = this.readMemory( + 'uint32', + this.gameAssembly.modBaseAddr, + this.offsets.clientId + ); + const allPlayersPtr = this.readMemory( 'ptr', this.gameAssembly.modBaseAddr, @@ -173,7 +185,8 @@ export default class GameReader { address + last, this.offsets.player.bufferLength ); - const player = this.parsePlayer(address + last, playerData); + + const player = this.parsePlayer(address + last, playerData, clientId); playerAddrPtr += this.is_64bit ? 8 : 4; if (!player) continue; players.push(player); @@ -215,17 +228,6 @@ export default class GameReader { } this.lastPlayerPtr = allPlayers; - const hostId = this.readMemory( - 'uint32', - this.gameAssembly.modBaseAddr, - this.offsets.hostId - ); - const clientId = this.readMemory( - 'uint32', - this.gameAssembly.modBaseAddr, - this.offsets.clientId - ); - const newState: AmongUsState = { lobbyCode: this.gameCode || 'MENU', players, @@ -405,7 +407,11 @@ export default class GameReader { ].join(''); } - parsePlayer(ptr: number, buffer: Buffer): Player | undefined { + parsePlayer( + ptr: number, + buffer: Buffer, + localClientId = -1 + ): Player | undefined { if (!this.PlayerStruct || !this.offsets) return undefined; const { data } = this.PlayerStruct.report(buffer, 0, {}); @@ -419,12 +425,13 @@ export default class GameReader { ]); } - const isLocal = - this.readMemory( - 'int', - data.objectPtr, - this.offsets.player.isLocal - ) !== 0; + const clientId = this.readMemory( + 'uint32', + data.objectPtr, + this.offsets.player.clientId + ); + + const isLocal = clientId === localClientId; const positionOffsets = isLocal ? [this.offsets.player.localX, this.offsets.player.localY] @@ -440,11 +447,6 @@ export default class GameReader { data.objectPtr, positionOffsets[1] ); - const clientId = this.readMemory( - 'uint32', - data.objectPtr, - this.offsets.clientId - ); return { ptr, diff --git a/src/main/offsetStore.ts b/src/main/offsetStore.ts index 41c96e12..ba25de44 100644 --- a/src/main/offsetStore.ts +++ b/src/main/offsetStore.ts @@ -23,7 +23,6 @@ export interface IOffsets { hostId: number[]; clientId: number[]; player: { - isLocal: number[]; localX: number[]; localY: number[]; remoteX: number[]; @@ -31,6 +30,7 @@ export interface IOffsets { bufferLength: number; offsets: number[]; inVent: number[]; + clientId: number[]; struct: { type: | 'INT' @@ -91,7 +91,6 @@ export default { { type: 'UINT', name: 'objectPtr' }, { type: 'SKIP', skip: 4, name: 'unused' }, ], - isLocal: [120], localX: [144, 108], localY: [144, 112], remoteX: [144, 88], @@ -151,7 +150,6 @@ export default { { type: 'SKIP', skip: 2, name: 'unused' }, { type: 'UINT', name: 'objectPtr' }, ], - isLocal: [84], localX: [96, 80], localY: [96, 84], remoteX: [96, 60], From 151afe010fdce12fb008ff937a7c3d674c07f117 Mon Sep 17 00:00:00 2001 From: fabiryn Date: Tue, 29 Dec 2020 02:32:38 +0100 Subject: [PATCH 61/88] fix keyboard shortcuts (F-keys) --- src/renderer/settings/Settings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/settings/Settings.tsx b/src/renderer/settings/Settings.tsx index c338ab3d..482534d6 100644 --- a/src/renderer/settings/Settings.tsx +++ b/src/renderer/settings/Settings.tsx @@ -472,7 +472,7 @@ const Settings: React.FC = function ({ if (k === 'Control' || k === 'Alt' || k === 'Shift') k = (ev.location === 1 ? 'L' : 'R') + k; - if (/^[0-9A-Z]$/.test(k) || /^F[0-9]{1, 2}$/.test(k) || keys.has(k)) { + if (/^[0-9A-Z]$/.test(k) || /^F[0-9]{1,2}$/.test(k) || keys.has(k)) { setSettings({ type: 'setOne', action: [shortcut, k], From 23a6e02b3ffbd9557a65e7b0277e4224b90d078b Mon Sep 17 00:00:00 2001 From: Ottomated Date: Tue, 29 Dec 2020 20:51:52 -0800 Subject: [PATCH 62/88] support old version, bump to 1.2.1 --- package.json | 2 +- src/main/offsetStore.ts | 84 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index f7400593..0470aa6c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "crewlink", - "version": "1.2.0", + "version": "1.2.1", "license": "GPL-3.0-or-later", "description": "Free, open, Among Us proximity voice chat", "repository": { diff --git a/src/main/offsetStore.ts b/src/main/offsetStore.ts index f4addb87..3623a1d7 100644 --- a/src/main/offsetStore.ts +++ b/src/main/offsetStore.ts @@ -45,6 +45,90 @@ export interface IOffsets { } export default { + 'lagz++MaYU+z5QoxU9US54EQe9HVGPo9rZ8DTisw8tc=': { + versionNumber: '2020.10.22', + versionSource: 'steam', + offsets: { + meetingHud: [21280716, 92, 0], + meetingHudCachePtr: [8], + meetingHudState: [132], + gameState: [21281584, 92, 0, 100], + hostId: [21281584, 92, 0, 68], + clientId: [21281584, 92, 0, 72], + allPlayersPtr: [21281328, 92, 0, 36], + allPlayers: [8], + playerCount: [12], + playerAddrPtr: 16, + exiledPlayerId: [255, 21280716, 92, 0, 148, 8], + gameCode: [20607324, 92, 0, 32, 40], + player: { + struct: [ + { + type: 'SKIP', + skip: 8, + name: 'unused', + }, + { + type: 'UINT', + name: 'id', + }, + { + type: 'UINT', + name: 'name', + }, + { + type: 'UINT', + name: 'color', + }, + { + type: 'UINT', + name: 'hat', + }, + { + type: 'UINT', + name: 'pet', + }, + { + type: 'UINT', + name: 'skin', + }, + { + type: 'UINT', + name: 'disconnected', + }, + { + type: 'UINT', + name: 'taskPtr', + }, + { + type: 'BYTE', + name: 'impostor', + }, + { + type: 'BYTE', + name: 'dead', + }, + { + type: 'SKIP', + skip: 2, + name: 'unused', + }, + { + type: 'UINT', + name: 'objectPtr', + }, + ], + isLocal: [84], + localX: [96, 80], + localY: [96, 84], + remoteX: [96, 60], + remoteY: [96, 64], + bufferLength: 56, + offsets: [0, 0], + inVent: [49], + }, + }, + }, 'CwEL0xldOcCJ3AGNg0suvSa6Z9L0nE6+pgioBPwJdbc=': { versionNumber: '2020.12.9', versionSource: 'steam', From 4e8e2912b5e11e9735606c73187380d7c1449c77 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Tue, 29 Dec 2020 22:00:13 -0800 Subject: [PATCH 63/88] merge --- src/renderer/Overlay.tsx | 185 +---------------------------- src/renderer/ViewManager.tsx | 51 -------- src/renderer/settings/Settings.tsx | 8 ++ 3 files changed, 9 insertions(+), 235 deletions(-) delete mode 100644 src/renderer/ViewManager.tsx diff --git a/src/renderer/Overlay.tsx b/src/renderer/Overlay.tsx index 31ac7526..c6f47339 100644 --- a/src/renderer/Overlay.tsx +++ b/src/renderer/Overlay.tsx @@ -1,191 +1,8 @@ import React, { useEffect, useMemo, useState } from 'react'; import { ipcRenderer } from 'electron'; import { GameState, AmongUsState, Player } from '../common/AmongUsState'; -import Avatar from './Avatar'; import { ISettings } from '../common/ISettings'; -interface OtherTalking { - [playerId: number]: boolean; -} - -interface OtherDead { - [playerId: number]: boolean; -} - -interface SocketIdMap { - [socketId: string]: number; -} - export default function Overlay() { - const [status, setStatus] = useState("WAITING"); - const [gameState, setGameState] = useState({} as AmongUsState); - const [settings, setSettings] = useState({} as ISettings); - const [socketPlayerIds, setSocketPlayerIds] = useState({}); - const [talking, setTalking] = useState(false); - const [otherTalking, setOtherTalking] = useState({}); - const [otherDead, setOtherDead] = useState({}); - const myPlayer = useMemo(() => { - if (!gameState || !gameState.players) return undefined; - else return gameState.players.find(p => p.isLocal); - }, [gameState]); - - const relevantPlayers = useMemo(() => { - let relevantPlayers: Player[]; - if (!gameState || !gameState.players || gameState.lobbyCode === 'MENU' || !myPlayer) relevantPlayers = []; - else relevantPlayers = gameState.players.filter(p => ( - (Object.values(socketPlayerIds).includes(p.id) || p.isLocal) && - ((!myPlayer.isDead && !otherDead[p.id]) || myPlayer.isDead) - )); - return relevantPlayers; - }, [gameState]); - - let talkingPlayers: Player[]; - if (!gameState || !gameState.players || gameState.lobbyCode === 'MENU' || !myPlayer) talkingPlayers = []; - else talkingPlayers = gameState.players.filter(p => (otherTalking[p.id] || (p.isLocal && talking))); - - useEffect(() => { - if (gameState.gameState === GameState.LOBBY) { - setOtherDead({}); - } else if (gameState.gameState !== GameState.TASKS) { - if (!gameState.players) return; - setOtherDead(old => { - for (let player of gameState.players) { - old[player.id] = player.isDead || player.disconnected; - } - return { ...old }; - }); - } - }, [gameState.gameState]); - - useEffect(() => { - const onOverlaySettings = (_: Electron.IpcRendererEvent, newSettings: any) => { - setSettings(newSettings); - }; - - const onOverlayState = (_: Electron.IpcRendererEvent, state: string) => { - setStatus(state); - }; - - const onOverlayGameState = (_: Electron.IpcRendererEvent, newState: AmongUsState) => { - setGameState(newState); - }; - - const onOverlaySocketIds = (_: Electron.IpcRendererEvent, ids: SocketIdMap) => { - setSocketPlayerIds(ids); - }; - - - const onOverlayTalkingSelf = (_: Electron.IpcRendererEvent, talking: boolean) => { - setTalking(talking); - }; - - const onOverlayTalking = (_: Electron.IpcRendererEvent, id: number) => { - setOtherTalking(old => ({ - ...old, - [id]: true - })); - }; - - const onOverlayNotTalking = (_: Electron.IpcRendererEvent, id: number) => { - setOtherTalking(old => ({ - ...old, - [id]: false - })); - - }; - - ipcRenderer.on('overlaySettings', onOverlaySettings); - ipcRenderer.on('overlayState', onOverlayState); - ipcRenderer.on('overlayGameState', onOverlayGameState); - ipcRenderer.on('overlaySocketIds', onOverlaySocketIds); - ipcRenderer.on('overlayTalkingSelf', onOverlayTalkingSelf); - ipcRenderer.on('overlayTalking', onOverlayTalking); - ipcRenderer.on('overlayNotTalking', onOverlayNotTalking); - return () => { - ipcRenderer.off('overlaySettings', onOverlaySettings); - ipcRenderer.off('overlayState', onOverlayState); - ipcRenderer.off('overlayGameState', onOverlayGameState); - ipcRenderer.off('overlaySocketIds', onOverlaySocketIds); - ipcRenderer.off('overlayTalkingSelf', onOverlayTalkingSelf); - ipcRenderer.off('overlayTalking', onOverlayTalking); - ipcRenderer.off('overlayNotTalking', onOverlayNotTalking); - } - }, []); - - document.body.style.backgroundColor = "rgba(255, 255, 255, 0)"; - document.body.style.paddingTop = "0"; - - var baseCSS:any = { - backgroundColor: "rgba(0, 0, 0, 0.85)", - width: "100px", - borderRadius: "8px", - position: "relative", - marginTop: "-16px", - paddingLeft: "8px", - }; - var topArea =

CrewLink ({status})

- var playersCSS:any = {} - var playerList:Player[] = []; - if (gameState.players && gameState.gameState != GameState.MENU) playerList = relevantPlayers; - - if (gameState.gameState == GameState.UNKNOWN || gameState.gameState == GameState.MENU) { - baseCSS["left"] = "8px"; - baseCSS["top"] = "60px"; - } else { - baseCSS["paddingTop"] = "8px"; - baseCSS["paddingLeft"] = "0px"; - baseCSS["width"] = "800px"; - baseCSS["backgroundColor"] = "rgba(0, 0, 0, 0.5)"; - if (settings.overlayPosition == 'top') { - baseCSS["marginLeft"] = "auto"; - baseCSS["marginRight"] = "auto"; - baseCSS["marginTop"] = "0px"; - } else if (settings.overlayPosition == 'bottom_left') { - baseCSS["position"] = "absolute"; - baseCSS["bottom"] = "0px"; - baseCSS["backgroundColor"] = "rgba(0, 0, 0, 0.35)"; - baseCSS["width"] = null; - - playersCSS["justifyContent"] = "left" - playersCSS["alignItems"] = "left" - } - topArea = <>; - if ((settings.compactOverlay || gameState.gameState == GameState.TASKS) && playerList) { - playerList = talkingPlayers; - baseCSS["backgroundColor"] = "rgba(0, 0, 0, 0)"; - } - } - - var playerArea:JSX.Element = <>; - if (playerList) { - playerArea =
- { - playerList.map(player => { - const connected = Object.values(socketPlayerIds).includes(player.id) || player.isLocal; - let name = settings.compactOverlay ? "" : {player.name} - return ( -
-
- -
- {name} -
- ); - }) - } -
- } - - - - return ( -
- {topArea} - {playerArea} -
- ) + return
; } \ No newline at end of file diff --git a/src/renderer/ViewManager.tsx b/src/renderer/ViewManager.tsx deleted file mode 100644 index e413c503..00000000 --- a/src/renderer/ViewManager.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React, { Component } from 'react'; -// @ts-ignore -import { BrowserRouter as Router, Route } from 'react-router-dom' -import { ipcRenderer } from 'electron'; -import ReactDOM from 'react-dom'; -import App from './App'; -import Overlay from './Overlay'; - -// @ts-ignore -class ViewManager extends Component { - - static Views() { - // @ts-ignore - var val:any = { - // @ts-ignore - app: , - // @ts-ignore - overlay: - } - return val - } - - static View(props: any) { - let name = props.location.search.split("view=")[1]; - // @ts-ignore - console.log("View type: " + name); - let view = ViewManager.Views()[name]; - if(view == null) - throw new Error("View '" + name + "' is undefined"); - //console.log("View is not null"); - if (name == "app") { - ipcRenderer.send('start'); - console.log("Sent ipcRenderer start"); - } - return view; - } - - render() { - return ( - -
- -
-
- ); - } -} - -export default ViewManager - -ReactDOM.render(, document.getElementById('app')); \ No newline at end of file diff --git a/src/renderer/settings/Settings.tsx b/src/renderer/settings/Settings.tsx index 482534d6..a308eb34 100644 --- a/src/renderer/settings/Settings.tsx +++ b/src/renderer/settings/Settings.tsx @@ -226,6 +226,14 @@ const store = new Store({ maxDistance: 5.32, }, }, + compactOverlay: { + type: 'boolean', + default: false + }, + overlayPosition: { + type: 'string', + default: 'top' + } }, }); From cd3659302f201d2297532e47668a76c388912c49 Mon Sep 17 00:00:00 2001 From: Sandro Habicher Date: Sat, 2 Jan 2021 14:31:48 +0100 Subject: [PATCH 64/88] Refactored voice calculation. --- src/renderer/Voice.tsx | 96 +++++++++++++++++++++++++----------------- 1 file changed, 57 insertions(+), 39 deletions(-) diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index d830c868..01e4126c 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -88,52 +88,70 @@ function calculateVoiceAudio( pan: PannerNode ): void { const audioContext = pan.context; - pan.positionZ.setValueAtTime(-0.5, audioContext.currentTime); let panPos = [other.x - me.x, other.y - me.y]; - if ( - state.gameState === GameState.DISCUSSION || - (state.gameState === GameState.LOBBY && !settings.enableSpatialAudio) - ) { + + switch (state.gameState) { + case GameState.MENU: + gain.gain.value = 0; + break; + + case GameState.LOBBY: + gain.gain.value = 1; + break; + + case GameState.TASKS: + gain.gain.value = 1; + + // Mute other players which are in a vent + if (other.inVent) { + gain.gain.value = 0; + } + + // Mute dead players for still living players + if (!me.isDead && other.isDead) { + gain.gain.value = 0; + } + + break; + + case GameState.DISCUSSION: + panPos = [0, 0]; + gain.gain.value = 1; + + // Mute dead players for still living players + if (!me.isDead && other.isDead) { + gain.gain.value = 0; + } + + break; + + case GameState.UNKNOWN: + default: + gain.gain.value = 0; + break; + } + + // Reset panning position if the setting is disabled + if (!settings.enableSpatialAudio) { panPos = [0, 0]; } + + // Clamp panning position if (isNaN(panPos[0])) panPos[0] = 999; if (isNaN(panPos[1])) panPos[1] = 999; - panPos[0] = Math.min(999, Math.max(-999, panPos[0])); - panPos[1] = Math.min(999, Math.max(-999, panPos[1])); - if (other.inVent) { - gain.gain.value = 0; - return; - } - if (me.isDead && other.isDead) { - gain.gain.value = 1; - pan.positionX.setValueAtTime(panPos[0], audioContext.currentTime); - pan.positionY.setValueAtTime(panPos[1], audioContext.currentTime); - return; - } - if (!me.isDead && other.isDead) { - gain.gain.value = 0; - return; - } - if ( - state.gameState === GameState.LOBBY || - state.gameState === GameState.DISCUSSION - ) { - gain.gain.value = 1; - pan.positionX.setValueAtTime(panPos[0], audioContext.currentTime); - pan.positionY.setValueAtTime(panPos[1], audioContext.currentTime); - } else if (state.gameState === GameState.TASKS) { - gain.gain.value = 1; - pan.positionX.setValueAtTime(panPos[0], audioContext.currentTime); - pan.positionY.setValueAtTime(panPos[1], audioContext.currentTime); - } else { - gain.gain.value = 0; - } - if ( - gain.gain.value === 1 && - Math.sqrt(Math.pow(panPos[0], 2) + Math.pow(panPos[1], 2)) > 7 - ) { + + panPos[0] = Math.min(Math.max(panPos[0], -999), 999); + panPos[1] = Math.min(Math.max(panPos[1], -999), 999); + + // Mute players if distancte between two players is too big + if (Math.sqrt(Math.pow(panPos[0], 2) + Math.pow(panPos[1], 2)) > 7) { gain.gain.value = 0; } + + // Apply position's to PanNode + pan.positionX.setValueAtTime(panPos[0], audioContext.currentTime); + pan.positionY.setValueAtTime(panPos[1], audioContext.currentTime); + pan.positionZ.setValueAtTime(-0.5, audioContext.currentTime); } export interface VoiceProps { From 5ec23397d49de2e3e7f597868bf97fb7d698d9a7 Mon Sep 17 00:00:00 2001 From: Psy Tauon Date: Tue, 5 Jan 2021 04:21:11 -0700 Subject: [PATCH 65/88] Add up listener for mouse buttons and mute/deafen --- src/main/hook.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/hook.ts b/src/main/hook.ts index a853ec31..378ce835 100644 --- a/src/main/hook.ts +++ b/src/main/hook.ts @@ -81,6 +81,18 @@ ipcMain.handle(IpcHandlerMessages.START_HOOK, async (event) => { ) { event.sender.send(IpcRendererMessages.PUSH_TO_TALK, false); } + if ( + isMouseButton(store.get('deafenShortcut')) && + mouseClickMatches(store.get('deafenShortcut') as M, ev) + ) { + event.sender.send(IpcRendererMessages.TOGGLE_DEAFEN); + } + if ( + isMouseButton(store.get('muteShortcut', 'RAlt')) && + mouseClickMatches(store.get('muteShortcut', 'RAlt') as M, ev) + ) { + event.sender.send(IpcRendererMessages.TOGGLE_MUTE); + } }); iohook.start(); From 7e900dde946833a652dc8001c02b24c4af97514b Mon Sep 17 00:00:00 2001 From: Dor Date: Tue, 5 Jan 2021 20:05:37 +0200 Subject: [PATCH 66/88] Workaround for duplicate audio sources aka "can be heard anywhere" Also addresses OtherDead comment. --- src/renderer/Voice.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index d830c868..f40f0365 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -63,7 +63,7 @@ interface OtherTalking { } interface OtherDead { - [playerId: number]: boolean; // isTalking + [playerId: number]: boolean; // isDead } interface AudioConnected { @@ -443,6 +443,7 @@ const Voice: React.FC = function ({ const context = new AudioContext(); const source = context.createMediaStreamSource(stream); const gain = context.createGain(); + gain.value = 0; // Mute to start, workaround to address duplicate sources aka "can be heard anywhere" const pan = context.createPanner(); pan.refDistance = 0.1; pan.panningModel = 'equalpower'; From e4854e56c1ac945b00653d3b88271b762ea36d66 Mon Sep 17 00:00:00 2001 From: Dor Date: Tue, 5 Jan 2021 22:07:13 +0200 Subject: [PATCH 67/88] Accidental trim. --- src/renderer/Voice.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index f40f0365..dfa73878 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -443,7 +443,7 @@ const Voice: React.FC = function ({ const context = new AudioContext(); const source = context.createMediaStreamSource(stream); const gain = context.createGain(); - gain.value = 0; // Mute to start, workaround to address duplicate sources aka "can be heard anywhere" + gain.gain.value = 0; // Mute to start, workaround to address duplicate sources aka "can be heard anywhere" const pan = context.createPanner(); pan.refDistance = 0.1; pan.panningModel = 'equalpower'; From 02d2f8064b3c1c076e6e3f3f55b5e56fe6d7560f Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 9 Jan 2021 00:18:35 -0800 Subject: [PATCH 68/88] overlay done? --- src/common/AmongUsState.ts | 25 +++++ src/common/ipc-messages.ts | 7 ++ src/main/index.ts | 4 +- src/main/ipc-handlers.ts | 8 +- src/renderer/App.tsx | 6 + src/renderer/Overlay.tsx | 205 ++++++++++++++++++++++++++++++++++- src/renderer/Voice.tsx | 43 ++++---- src/renderer/css/overlay.css | 5 + src/renderer/index.ts | 2 - 9 files changed, 271 insertions(+), 34 deletions(-) create mode 100644 src/renderer/css/overlay.css diff --git a/src/common/AmongUsState.ts b/src/common/AmongUsState.ts index aac78c6b..12ee35d2 100644 --- a/src/common/AmongUsState.ts +++ b/src/common/AmongUsState.ts @@ -36,3 +36,28 @@ export enum GameState { MENU, UNKNOWN, } + +export interface Client { + playerId: number; + clientId: number; +} +export interface SocketClientMap { + [socketId: string]: Client; +} +export interface OtherTalking { + [playerId: number]: boolean; // isTalking +} + +export interface AudioConnected { + [peer: string]: boolean; // isConnected +} + +export interface VoiceState { + otherTalking: OtherTalking; + playerSocketIds: { + [index: number]: string; + }; + otherDead: OtherTalking; + socketClients: SocketClientMap; + audioConnected: AudioConnected; +} \ No newline at end of file diff --git a/src/common/ipc-messages.ts b/src/common/ipc-messages.ts index 9c152e19..19277052 100644 --- a/src/common/ipc-messages.ts +++ b/src/common/ipc-messages.ts @@ -6,6 +6,13 @@ export enum IpcMessages { OPEN_AMONG_US_GAME = 'OPEN_AMONG_US_GAME', RESTART_CREWLINK = 'RESTART_CREWLINK', QUIT_CREWLINK = 'QUIT_CREWLINK', + SEND_TO_OVERLAY = 'SEND_TO_OVERLAY', +} + +// Renderer 1 --> Overlay Window (send/on) +export enum IpcOverlayMessages { + NOTIFY_GAME_STATE_CHANGED = 'NOTIFY_GAME_STATE_CHANGED', + NOTIFY_VOICE_STATE_CHANGED = 'NOTIFY_VOICE_STATE_CHANGED' } // Renderer --> Main (sendSync/on) diff --git a/src/main/index.ts b/src/main/index.ts index c0308b27..09da316e 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -214,9 +214,9 @@ if (!gotTheLock) { // create main BrowserWindow when electron is ready app.whenReady().then(() => { - initializeIpcListeners(); - initializeIpcHandlers(); mainWindow = createMainWindow(); overlayWindow = createOverlay(); + initializeIpcListeners(overlayWindow); + initializeIpcHandlers(); }); } diff --git a/src/main/ipc-handlers.ts b/src/main/ipc-handlers.ts index 51d2dde7..2789c5eb 100644 --- a/src/main/ipc-handlers.ts +++ b/src/main/ipc-handlers.ts @@ -3,10 +3,10 @@ import { HKEY, enumerateValues } from 'registry-js'; import spawn from 'cross-spawn'; import path from 'path'; -import { IpcMessages } from '../common/ipc-messages'; +import { IpcMessages, IpcOverlayMessages } from '../common/ipc-messages'; // Listeners are fire and forget, they do not have "responses" or return values -export const initializeIpcListeners = (): void => { +export const initializeIpcListeners = (overlayWindow: BrowserWindow): void => { ipcMain.on( IpcMessages.SHOW_ERROR_DIALOG, (e, opts: { title: string; content: string }) => { @@ -56,6 +56,10 @@ export const initializeIpcListeners = (): void => { } app.quit(); }); + + ipcMain.on(IpcMessages.SEND_TO_OVERLAY, (_, event: IpcOverlayMessages, ...args: unknown[]) => { + overlayWindow.webContents.send(event, ...args); + }); }; // Handlers are async cross-process instructions, they should have a return value diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index bc077f39..38b13f48 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -24,6 +24,7 @@ import { AutoUpdaterState, IpcHandlerMessages, IpcMessages, + IpcOverlayMessages, IpcRendererMessages, IpcSyncMessages, } from '../common/ipc-messages'; @@ -40,6 +41,7 @@ import DialogContentText from '@material-ui/core/DialogContentText'; import DialogActions from '@material-ui/core/DialogActions'; import Button from '@material-ui/core/Button'; import prettyBytes from 'pretty-bytes'; +import './css/index.css'; let appVersion = ''; if (typeof window !== 'undefined' && window.location) { @@ -191,6 +193,10 @@ export default function App() { }; }, []); + useEffect(() => { + ipcRenderer.send(IpcMessages.SEND_TO_OVERLAY, IpcOverlayMessages.NOTIFY_GAME_STATE_CHANGED, gameState); + }, [gameState]); + let page; switch (state) { case AppState.MENU: diff --git a/src/renderer/Overlay.tsx b/src/renderer/Overlay.tsx index c6f47339..f4c54e38 100644 --- a/src/renderer/Overlay.tsx +++ b/src/renderer/Overlay.tsx @@ -1,8 +1,205 @@ import React, { useEffect, useMemo, useState } from 'react'; import { ipcRenderer } from 'electron'; -import { GameState, AmongUsState, Player } from '../common/AmongUsState'; -import { ISettings } from '../common/ISettings'; +import { AmongUsState, GameState, VoiceState, OtherTalking } from '../common/AmongUsState'; +import { IpcOverlayMessages } from '../common/ipc-messages'; +import ReactDOM from 'react-dom'; +import makeStyles from '@material-ui/core/styles/makeStyles'; +import './css/overlay.css' +import Avatar from './Avatar'; + +interface UseStylesProps { + hudHeight: number; +} + +const useStyles = makeStyles((theme) => ({ + meetingHud: { + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)' + }, + playerIcons: { + width: '83.45%', + height: '63.2%', + left: '5%', + top: '18.4703%', + position: 'absolute', + display: 'flex', + '&>*:nth-child(odd)': { + marginRight: '1.4885%' + }, + '&>*:nth-child(even)': { + marginLeft: '1.4885%' + }, + flexWrap: 'wrap' + }, + icon: { + width: '48.51%', + height: '16.49%', + borderRadius: ({ hudHeight }: UseStylesProps) => hudHeight / 100, + transition: 'opacity .1s linear', + marginBottom: '2.25%', + boxSizing: 'border-box' + } +})); + + + +function useWindowSize() { + const [windowSize, setWindowSize] = useState<[number, number]>([0, 0]); + + useEffect(() => { + const onResize = () => { + setWindowSize([window.innerWidth, window.innerHeight]); + }; + window.addEventListener('resize', onResize); + onResize(); + + return () => window.removeEventListener('resize', onResize); + }, []); + return windowSize; +} + +const playerColors = [ + ['#C51111', '#7A0838',], + ['#132ED1', '#09158E',], + ['#117F2D', '#0A4D2E',], + ['#ED54BA', '#AB2BAD',], + ['#EF7D0D', '#B33E15',], + ['#F5F557', '#C38823',], + ['#3F474E', '#1E1F26',], + ['#8394BF', '#8394BF',], + ['#6B2FBB', '#3B177C',], + ['#71491E', '#5E2615',], + ['#38FEDC', '#24A8BE',], + ['#50EF39', '#15A742',] +]; + +const iPadRatio = 854 / 579; export default function Overlay() { - return
; -} \ No newline at end of file + const [gameState, setGameState] = useState({} as AmongUsState); + const [voiceState, setVoiceState] = useState({} as VoiceState); + useEffect(() => { + const onState = (_: Electron.IpcRendererEvent, newState: AmongUsState) => { + setGameState(newState); + }; + const onVoiceState = (_: Electron.IpcRendererEvent, newState: VoiceState) => { + setVoiceState(newState); + }; + ipcRenderer.on(IpcOverlayMessages.NOTIFY_GAME_STATE_CHANGED, onState); + ipcRenderer.on(IpcOverlayMessages.NOTIFY_VOICE_STATE_CHANGED, onVoiceState); + return () => { + ipcRenderer.off(IpcOverlayMessages.NOTIFY_GAME_STATE_CHANGED, onState); + ipcRenderer.off(IpcOverlayMessages.NOTIFY_VOICE_STATE_CHANGED, onVoiceState); + } + }, []); + + return ( + <> + + + + ); +} + +interface AvatarOverlayProps { + voiceState: VoiceState; + gameState: AmongUsState; +} + +const useOverlayStyles = makeStyles((theme) => ({ + root: { + width: '5%', + position: 'absolute', + top: '50%', + right: 0, + transform: 'translateY(-50%)', + background: '#25232ac0', + padding: theme.spacing(2), + borderTopLeftRadius: 20, + borderBottomLeftRadius: 20, + } +})); +const AvatarOverlay: React.FC = ({ voiceState, gameState }: AvatarOverlayProps) => { + if (!gameState.players) return null; + const classes = useOverlayStyles(); + const avatars : JSX.Element[] = []; + + gameState.players.forEach(player => { + if (!voiceState.otherTalking[player.id]) return; + const peer = voiceState.playerSocketIds[player.id]; + const connected = Object.values(voiceState.socketClients) + .map(({ playerId }) => playerId) + .includes(player.id); + const audio = voiceState.audioConnected[peer]; + avatars.push( + + ); + }); + if (avatars.length === 0) return null; + return ( +
+ {avatars} +
+ ) +}; + +interface MeetingHudProps { + otherTalking: OtherTalking; + gameState: AmongUsState; +} + +const MeetingHud: React.FC = ({ otherTalking, gameState }: MeetingHudProps) => { + const [width, height] = useWindowSize(); + + let hudWidth = 0, hudHeight = 0; + if (width / (height * 0.96) > iPadRatio) { + hudHeight = height * 0.96; + hudWidth = hudHeight * iPadRatio; + } else { + hudWidth = width; + hudHeight = width * (1 / iPadRatio); + } + const classes = useStyles({ hudHeight }); + const players = useMemo(() => { + if (!gameState.players) return null; + return gameState.players.sort((a, b) => { + if ((a.disconnected || a.isDead) && (b.disconnected || b.isDead)) { + return a.id - b.id; + } else if (a.disconnected || a.isDead) { + return 1000; + } else if (b.disconnected || b.isDead) { + return -1000; + } + return a.id - b.id; + }) + }, [gameState.players]); + if (!players || gameState.gameState !== GameState.DISCUSSION) return null; + const overlays = gameState.players.map((player) => { + return ( +
+ ); + }); + + return
+
+ {overlays} +
+
; +} + +ReactDOM.render(, document.getElementById('app')); \ No newline at end of file diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index d830c868..e71cf8fb 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -6,12 +6,12 @@ import { LobbySettingsContext, SettingsContext, } from './contexts'; -import { AmongUsState, GameState, Player } from '../common/AmongUsState'; +import { AmongUsState, AudioConnected, Client, GameState, OtherTalking, Player, SocketClientMap, VoiceState } from '../common/AmongUsState'; import Peer from 'simple-peer'; import { ipcRenderer } from 'electron'; import VAD from './vad'; import { ISettings } from '../common/ISettings'; -import { IpcRendererMessages } from '../common/ipc-messages'; +import { IpcMessages, IpcOverlayMessages, IpcRendererMessages } from '../common/ipc-messages'; import Typography from '@material-ui/core/Typography'; import Grid from '@material-ui/core/Grid'; import makeStyles from '@material-ui/core/styles/makeStyles'; @@ -46,9 +46,6 @@ interface AudioElements { }; } -interface SocketClientMap { - [socketId: string]: Client; -} interface ConnectionStuff { socket?: typeof Socket; @@ -58,23 +55,6 @@ interface ConnectionStuff { muted: boolean; } -interface OtherTalking { - [playerId: number]: boolean; // isTalking -} - -interface OtherDead { - [playerId: number]: boolean; // isTalking -} - -interface AudioConnected { - [peer: string]: boolean; // isConnected -} - -interface Client { - playerId: number; - clientId: number; -} - interface SocketError { message?: string; } @@ -217,7 +197,7 @@ const Voice: React.FC = function ({ connect: (lobbyCode: string, playerId: number, clientId: number) => void; } | null>(null); const [otherTalking, setOtherTalking] = useState({}); - const [otherDead, setOtherDead] = useState({}); + const [otherDead, setOtherDead] = useState({}); const audioElements = useRef({}); const [audioConnected, setAudioConnected] = useState({}); const classes = useStyles(); @@ -242,6 +222,7 @@ const Voice: React.FC = function ({ delete audioElements.current[peer]; } } + // Handle pushToTalk, if set useEffect(() => { if (!connectionStuff.current.stream) return; @@ -460,6 +441,7 @@ const Voice: React.FC = function ({ }); const setTalking = (talking: boolean) => { + if (!socketClientsRef.current[peer]) return; setOtherTalking((old) => ({ ...old, [socketClientsRef.current[peer].playerId]: @@ -648,7 +630,7 @@ const Voice: React.FC = function ({ ); } }, [myPlayer?.id]); - + const playerSocketIds: { [index: number]: string; } = {}; @@ -657,6 +639,19 @@ const Voice: React.FC = function ({ if (socketClients[k].playerId !== undefined) playerSocketIds[socketClients[k].playerId] = k; } + + // Pass voice state to overlay + useEffect(() => { + ipcRenderer.send(IpcMessages.SEND_TO_OVERLAY, IpcOverlayMessages.NOTIFY_VOICE_STATE_CHANGED, + { + otherTalking, + playerSocketIds, + otherDead, + socketClients, + audioConnected + } as VoiceState); + }, [otherTalking, playerSocketIds, otherDead, socketClients, audioConnected]); + return (
{error && ( diff --git a/src/renderer/css/overlay.css b/src/renderer/css/overlay.css new file mode 100644 index 00000000..7f50abaf --- /dev/null +++ b/src/renderer/css/overlay.css @@ -0,0 +1,5 @@ +body, #app { + width: 100vw; + height: 100vh; + margin: 0; +} \ No newline at end of file diff --git a/src/renderer/index.ts b/src/renderer/index.ts index b8f7ac2c..8f278ce3 100644 --- a/src/renderer/index.ts +++ b/src/renderer/index.ts @@ -1,5 +1,3 @@ -import './css/index.css'; - if (typeof window !== 'undefined' && window.location) { const query = new URLSearchParams(window.location.search.substring(1)); const view = query.get('view') || 'app'; From 0c0f5308889b96f8e129d5b740b4c7250d061400 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 9 Jan 2021 00:38:51 -0800 Subject: [PATCH 69/88] Add keys --- src/renderer/Overlay.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/renderer/Overlay.tsx b/src/renderer/Overlay.tsx index f4c54e38..0ad41023 100644 --- a/src/renderer/Overlay.tsx +++ b/src/renderer/Overlay.tsx @@ -124,8 +124,8 @@ const useOverlayStyles = makeStyles((theme) => ({ const AvatarOverlay: React.FC = ({ voiceState, gameState }: AvatarOverlayProps) => { if (!gameState.players) return null; const classes = useOverlayStyles(); - const avatars : JSX.Element[] = []; - + const avatars: JSX.Element[] = []; + gameState.players.forEach(player => { if (!voiceState.otherTalking[player.id]) return; const peer = voiceState.playerSocketIds[player.id]; @@ -135,6 +135,7 @@ const AvatarOverlay: React.FC = ({ voiceState, gameState }: const audio = voiceState.audioConnected[peer]; avatars.push( = ({ otherTalking, gameState }: Meet if (!players || gameState.gameState !== GameState.DISCUSSION) return null; const overlays = gameState.players.map((player) => { return ( -
= ({ otherTalking, gameState }: Meet ); }); + while (overlays.length < 10) { + overlays.push(
); + } + return
{overlays} From 2f3fec10f326c6ced1ab8e64e121a231c0f6d637 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 9 Jan 2021 01:41:59 -0800 Subject: [PATCH 70/88] Overlay configuration --- src/common/ISettings.d.ts | 4 +-- src/common/ipc-messages.ts | 3 +- src/renderer/App.tsx | 8 +++-- src/renderer/Avatar.tsx | 4 ++- src/renderer/Overlay.tsx | 47 ++++++++++++++++++++------- src/renderer/settings/Settings.tsx | 51 ++++++++++++++++++++++++++---- 6 files changed, 93 insertions(+), 24 deletions(-) diff --git a/src/common/ISettings.d.ts b/src/common/ISettings.d.ts index 69ea15fa..9ae85f3e 100644 --- a/src/common/ISettings.d.ts +++ b/src/common/ISettings.d.ts @@ -9,8 +9,8 @@ export interface ISettings { muteShortcut: string; hideCode: boolean; enableSpatialAudio: boolean; - compactOverlay: boolean; - overlayPosition: string; + meetingOverlay: boolean; + overlayPosition: 'left' | 'right' | 'hidden'; localLobbySettings: ILobbySettings; } diff --git a/src/common/ipc-messages.ts b/src/common/ipc-messages.ts index 19277052..e9c01e7b 100644 --- a/src/common/ipc-messages.ts +++ b/src/common/ipc-messages.ts @@ -12,7 +12,8 @@ export enum IpcMessages { // Renderer 1 --> Overlay Window (send/on) export enum IpcOverlayMessages { NOTIFY_GAME_STATE_CHANGED = 'NOTIFY_GAME_STATE_CHANGED', - NOTIFY_VOICE_STATE_CHANGED = 'NOTIFY_VOICE_STATE_CHANGED' + NOTIFY_VOICE_STATE_CHANGED = 'NOTIFY_VOICE_STATE_CHANGED', + NOTIFY_SETTINGS_CHANGED = 'NOTIFY_SETTINGS_CHANGED' } // Renderer --> Main (sendSync/on) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 38b13f48..c61ee945 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -132,8 +132,8 @@ export default function App() { muteShortcut: 'RAlt', hideCode: false, enableSpatialAudio: true, - compactOverlay: false, - overlayPosition: 'top', + meetingOverlay: true, + overlayPosition: 'right', localLobbySettings: { maxDistance: 5.32, }, @@ -197,6 +197,10 @@ export default function App() { ipcRenderer.send(IpcMessages.SEND_TO_OVERLAY, IpcOverlayMessages.NOTIFY_GAME_STATE_CHANGED, gameState); }, [gameState]); + useEffect(() => { + ipcRenderer.send(IpcMessages.SEND_TO_OVERLAY, IpcOverlayMessages.NOTIFY_SETTINGS_CHANGED, settings[0]); + }, [settings]); + let page; switch (state) { case AppState.MENU: diff --git a/src/renderer/Avatar.tsx b/src/renderer/Avatar.tsx index ca55e6f5..d991ca2a 100644 --- a/src/renderer/Avatar.tsx +++ b/src/renderer/Avatar.tsx @@ -58,6 +58,7 @@ export interface AvatarProps { deafened?: boolean; muted?: boolean; connectionState?: 'disconnected' | 'novoice' | 'connected'; + style?: React.CSSProperties; } const Avatar: React.FC = function ({ @@ -69,6 +70,7 @@ const Avatar: React.FC = function ({ player, size, connectionState, + style }: AvatarProps) { const status = isAlive ? 'alive' : 'dead'; let image = players[status][player.colorId]; @@ -103,7 +105,7 @@ const Avatar: React.FC = function ({ return ( -
+
({} as AmongUsState); - const [voiceState, setVoiceState] = useState({} as VoiceState); + const [gameState, setGameState] = useState(undefined as unknown as AmongUsState); + const [voiceState, setVoiceState] = useState(undefined as unknown as VoiceState); + const [settings, setSettings] = useState(undefined as unknown as ISettings); useEffect(() => { const onState = (_: Electron.IpcRendererEvent, newState: AmongUsState) => { setGameState(newState); @@ -87,18 +89,30 @@ export default function Overlay() { const onVoiceState = (_: Electron.IpcRendererEvent, newState: VoiceState) => { setVoiceState(newState); }; + const onSettings = (_: Electron.IpcRendererEvent, newState: ISettings) => { + setSettings(newState); + }; ipcRenderer.on(IpcOverlayMessages.NOTIFY_GAME_STATE_CHANGED, onState); ipcRenderer.on(IpcOverlayMessages.NOTIFY_VOICE_STATE_CHANGED, onVoiceState); + ipcRenderer.on(IpcOverlayMessages.NOTIFY_SETTINGS_CHANGED, onSettings); return () => { ipcRenderer.off(IpcOverlayMessages.NOTIFY_GAME_STATE_CHANGED, onState); ipcRenderer.off(IpcOverlayMessages.NOTIFY_VOICE_STATE_CHANGED, onVoiceState); + ipcRenderer.off(IpcOverlayMessages.NOTIFY_SETTINGS_CHANGED, onSettings); } }, []); + if (!settings || !voiceState || !gameState) return null; return ( <> - - + { + settings.meetingOverlay && + + } + { + settings.overlayPosition !== 'hidden' && + + } ); } @@ -106,24 +120,24 @@ export default function Overlay() { interface AvatarOverlayProps { voiceState: VoiceState; gameState: AmongUsState; + position: ISettings["overlayPosition"] } const useOverlayStyles = makeStyles((theme) => ({ root: { width: '5%', position: 'absolute', - top: '50%', - right: 0, - transform: 'translateY(-50%)', background: '#25232ac0', padding: theme.spacing(2), - borderTopLeftRadius: 20, - borderBottomLeftRadius: 20, + '&>*': { + marginTop: 4, + marginBottom: 4 + } } })); -const AvatarOverlay: React.FC = ({ voiceState, gameState }: AvatarOverlayProps) => { +const AvatarOverlay: React.FC = ({ voiceState, gameState, position }: AvatarOverlayProps) => { if (!gameState.players) return null; - const classes = useOverlayStyles(); + const classes = useOverlayStyles({ position }); const avatars: JSX.Element[] = []; gameState.players.forEach(player => { @@ -149,7 +163,16 @@ const AvatarOverlay: React.FC = ({ voiceState, gameState }: }); if (avatars.length === 0) return null; return ( -
+
{avatars}
) diff --git a/src/renderer/settings/Settings.tsx b/src/renderer/settings/Settings.tsx index a308eb34..cee6332f 100644 --- a/src/renderer/settings/Settings.tsx +++ b/src/renderer/settings/Settings.tsx @@ -132,7 +132,7 @@ const keys = new Set([ 'RAlt', ]); -const store = new Store({ +const storeConfig : Store.Options = { migrations: { '1.1.3': (store) => { const serverIP = store.get('serverIP'); @@ -226,16 +226,19 @@ const store = new Store({ maxDistance: 5.32, }, }, - compactOverlay: { + meetingOverlay: { type: 'boolean', - default: false + default: true }, overlayPosition: { type: 'string', - default: 'top' + enum: ['left', 'right', 'hidden'], + default: 'right' } }, -}); +}; + +const store = new Store(storeConfig); export interface SettingsProps { open: boolean; @@ -333,7 +336,7 @@ const URLInput: React.FC = function ({ return ( <>
Audio From f838b7af0ea9d39144ec893119cfe7317d45c89d Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 9 Jan 2021 11:02:24 -0800 Subject: [PATCH 74/88] Add Error Boundary --- src/main/index.ts | 1 - src/renderer/App.tsx | 123 ++++++++++++++++++++++++++++------------- src/renderer/Voice.tsx | 6 +- 3 files changed, 89 insertions(+), 41 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index 09da316e..ebd97ba1 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -162,7 +162,6 @@ if (!gotTheLock) { height: 300, webPreferences: { nodeIntegration: true, - enableRemoteModule: true, webSecurity: false }, ...electronOverlayWindow.WINDOW_OPTS diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index b46fb97d..405f969a 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1,5 +1,6 @@ import React, { Dispatch, + ErrorInfo, SetStateAction, useEffect, useReducer, @@ -42,6 +43,8 @@ import DialogActions from '@material-ui/core/DialogActions'; import Button from '@material-ui/core/Button'; import prettyBytes from 'pretty-bytes'; import './css/index.css'; +import Typography from '@material-ui/core/Typography'; +import SupportLink from './SupportLink'; let appVersion = ''; if (typeof window !== 'undefined' && window.location) { @@ -113,6 +116,48 @@ enum AppState { VOICE, } +interface ErrorBoundaryState { + error?: Error; +} + +class ErrorBoundary extends React.Component<{}, ErrorBoundaryState> { + constructor(props: {}) { + super(props); + this.state = {}; + } + + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + // Update state so the next render will show the fallback UI. + return { error }; + } + + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error("React Error: ", error, errorInfo); + } + + render() { + if (this.state.error) { + return ( +
+ + REACT ERROR + + + {this.state.error.message} + {'\n'} + {this.state.error.stack} + + +
+ ); + } + + return this.props.children; + } +} + export default function App() { const [state, setState] = useState(AppState.MENU); const [gameState, setGameState] = useState({} as AmongUsState); @@ -146,7 +191,7 @@ export default function App() { useEffect(() => { const onOpen = (_: Electron.IpcRendererEvent, isOpen: boolean) => { - setState(isOpen ? AppState.VOICE : AppState.MENU); + setState(isOpen ? AppState.VOICE : AppState.MENU); }; const onState = (_: Electron.IpcRendererEvent, newState: AmongUsState) => { setGameState(newState); @@ -221,46 +266,48 @@ export default function App() { settingsOpen={settingsOpen} setSettingsOpen={setSettingsOpen} /> - setSettingsOpen(false)} - /> - - Updating... - - {(updaterState.state === 'downloading' || - updaterState.state === 'downloaded') && - updaterState.progress && ( - <> - - - {prettyBytes(updaterState.progress.transferred)} /{' '} - {prettyBytes(updaterState.progress.total)} - - + + setSettingsOpen(false)} + /> + + Updating... + + {(updaterState.state === 'downloading' || + updaterState.state === 'downloaded') && + updaterState.progress && ( + <> + + + {prettyBytes(updaterState.progress.transferred)} /{' '} + {prettyBytes(updaterState.progress.total)} + + + )} + {updaterState.state === 'error' && ( + + {updaterState.error} + )} + {updaterState.state === 'error' && ( - - {updaterState.error} - - )} - - {updaterState.state === 'error' && ( - - - - )} - - {page} + + )} + + {page} + diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index 16b4375c..f00cb64e 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -225,7 +225,9 @@ const Voice: React.FC = function ({ const [lobbySettings, setLobbySettings] = useContext(LobbySettingsContext); const lobbySettingsRef = useRef(lobbySettings); const gameState = useContext(GameStateContext); - let { lobbyCode: displayedLobbyCode } = gameState; + let displayedLobbyCode = ''; + if (gameState) + displayedLobbyCode = gameState.lobbyCode; if (displayedLobbyCode !== 'MENU' && settings.hideCode) displayedLobbyCode = 'LOBBY'; const [talking, setTalking] = useState(false); @@ -611,7 +613,7 @@ const Voice: React.FC = function ({ disconnectPeer(k); }); connectionStuff.current.socket?.close(); - audioListener.destroy(); + audioListener?.destroy(); }; }, []); From 569ddc2b63d7474f75b88ea1ae7b292952c7c084 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 9 Jan 2021 12:25:28 -0800 Subject: [PATCH 75/88] Toggle for hearing impostors in vents, improved error boundary --- src/common/ISettings.d.ts | 1 + src/renderer/App.tsx | 8 ++--- src/renderer/Voice.tsx | 4 +-- src/renderer/settings/Settings.tsx | 47 ++++++++++++++++++++++++++---- 4 files changed, 49 insertions(+), 11 deletions(-) diff --git a/src/common/ISettings.d.ts b/src/common/ISettings.d.ts index 9bf9a1c1..a85dcca3 100644 --- a/src/common/ISettings.d.ts +++ b/src/common/ISettings.d.ts @@ -17,4 +17,5 @@ export interface ISettings { export interface ILobbySettings { maxDistance: number; haunting: boolean; + hearImpostorsInVents: boolean; } diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 405f969a..365edbc4 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -144,12 +144,11 @@ class ErrorBoundary extends React.Component<{}, ErrorBoundaryState> { REACT ERROR - - {this.state.error.message} - {'\n'} + {this.state.error.stack} +
); } @@ -181,7 +180,8 @@ export default function App() { overlayPosition: 'right', localLobbySettings: { maxDistance: 5.32, - haunting: false + haunting: false, + hearImpostorsInVents: false }, }); const lobbySettings = useReducer( diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index f00cb64e..3cc1528b 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -87,7 +87,7 @@ function calculateVoiceAudio(state: AmongUsState, settings: ISettings, lobbySett gain.gain.value = 1; // Mute other players which are in a vent - if (other.inVent) { + if (other.inVent && !lobbySettings.hearImpostorsInVents) { gain.gain.value = 0; } @@ -116,7 +116,7 @@ function calculateVoiceAudio(state: AmongUsState, settings: ISettings, lobbySett } // Muffling in vents - if (me.inVent) { + if (me.inVent || other.inVent) { muffle.frequency.value = 1200; muffle.Q.value = 20; if (gain.gain.value === 1) diff --git a/src/renderer/settings/Settings.tsx b/src/renderer/settings/Settings.tsx index 5ab54238..510fec18 100644 --- a/src/renderer/settings/Settings.tsx +++ b/src/renderer/settings/Settings.tsx @@ -225,10 +225,15 @@ const storeConfig: Store.Options = { type: 'boolean', default: false, }, + hearImpostorsInVents: { + type: 'boolean', + default: false + } }, default: { maxDistance: 5.32, - haunting: false + haunting: false, + hearImpostorsInVents: false }, }, meetingOverlay: { @@ -600,11 +605,12 @@ const Settings: React.FC = function ({ } > { setSettings({ @@ -621,6 +627,37 @@ const Settings: React.FC = function ({ control={} /> + + { + setSettings({ + type: 'setLobbySetting', + action: ['hearImpostorsInVents', checked], + }); + if (gameState?.isHost) { + setLobbySettings({ + type: 'setOne', + action: ['hearImpostorsInVents', checked], + }); + } + }} + control={} + /> +
Audio From 5c7244384150a4f76a02fa0a063a5bbcd9754769 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 9 Jan 2021 12:25:37 -0800 Subject: [PATCH 76/88] tweak scrollbar corner --- src/renderer/css/index.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/renderer/css/index.css b/src/renderer/css/index.css index 13d7ce80..0c73a232 100644 --- a/src/renderer/css/index.css +++ b/src/renderer/css/index.css @@ -15,6 +15,7 @@ body { ::-webkit-scrollbar { width: 8px; + height: 8px; margin-left: 2px; } @@ -27,6 +28,10 @@ body { border-radius: 5px; } +::-webkit-scrollbar-corner { + opacity: 0; +} + ::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.3); } \ No newline at end of file From 35f9973fbfe727c744f6b8877c38578c17b7183d Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 9 Jan 2021 12:25:48 -0800 Subject: [PATCH 77/88] v2.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 49b0f16e..125076f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "crewlink", - "version": "1.2.1", + "version": "2.0.0", "license": "GPL-3.0-or-later", "description": "Free, open, Among Us proximity voice chat", "repository": { From 34805dce5076b9332d45b318884945b9fc2df8c9 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 9 Jan 2021 13:04:12 -0800 Subject: [PATCH 78/88] catch error when closing --- src/main/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/index.ts b/src/main/index.ts index ebd97ba1..2a20b97c 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -75,7 +75,9 @@ function createMainWindow() { window.on('closed', () => { mainWindow = null; if (overlayWindow != null) { - overlayWindow.close() + try { + overlayWindow.close(); + } catch (_) { } overlayWindow = null; } }); From 2a13efd93fd3b7527c0b32ab5c9e20b7be0a9659 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 9 Jan 2021 13:04:22 -0800 Subject: [PATCH 79/88] Initialize haunting to correct state --- src/renderer/Voice.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index 3cc1528b..f50e2329 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -519,7 +519,15 @@ const Voice: React.FC = function ({ reverbGain.gain.value = 0; reverb.buffer = convolverBuffer.current; - gain.connect(compressor); + if (lobbySettingsRef.current.haunting) { + gain.connect(compressor); + gain.connect(reverbGain); + reverbGain.connect(reverb); + reverb.connect(compressor); + } else { + gain.connect(compressor); + } + // Source -> pan -> muffle -> gain -> VAD -> destination VAD(context, compressor, context.destination, { onVoiceStart: () => setTalking(true), From d8c2b6a317879f2a82a47b8732c819e52db881eb Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 9 Jan 2021 13:58:26 -0800 Subject: [PATCH 80/88] Add comms sabotage option --- src/common/AmongUsState.ts | 8 +++ src/common/ISettings.d.ts | 1 + src/main/GameReader.ts | 80 +++++++++++++++++++++--------- src/main/index.ts | 5 ++ src/main/offsetStore.ts | 26 ++++++++++ src/renderer/App.tsx | 3 +- src/renderer/Voice.tsx | 18 ++++--- src/renderer/settings/Settings.tsx | 38 +++++++++++++- 8 files changed, 147 insertions(+), 32 deletions(-) diff --git a/src/common/AmongUsState.ts b/src/common/AmongUsState.ts index 12ee35d2..d6bed4f5 100644 --- a/src/common/AmongUsState.ts +++ b/src/common/AmongUsState.ts @@ -6,6 +6,7 @@ export interface AmongUsState { isHost: boolean; clientId: number; hostId: number; + commsSabotaged: boolean; } export interface Player { @@ -29,6 +30,13 @@ export interface Player { inVent: boolean; } +export enum MapType { + THE_SKELD, + MIRA_HQ, + POLUS, + UNKNOWN, +} + export enum GameState { LOBBY, TASKS, diff --git a/src/common/ISettings.d.ts b/src/common/ISettings.d.ts index a85dcca3..71191c1a 100644 --- a/src/common/ISettings.d.ts +++ b/src/common/ISettings.d.ts @@ -18,4 +18,5 @@ export interface ILobbySettings { maxDistance: number; haunting: boolean; hearImpostorsInVents: boolean; + commsSabotage: boolean; } diff --git a/src/main/GameReader.ts b/src/main/GameReader.ts index c59b1eaa..85a25b1e 100644 --- a/src/main/GameReader.ts +++ b/src/main/GameReader.ts @@ -11,7 +11,7 @@ import { } from 'memoryjs'; import Struct from 'structron'; import { IpcRendererMessages } from '../common/ipc-messages'; -import { GameState, AmongUsState, Player } from '../common/AmongUsState'; +import { GameState, AmongUsState, Player, MapType } from '../common/AmongUsState'; import equal from 'deep-equal'; import offsetStore, { IOffsets } from './offsetStore'; import Errors from '../common/Errors'; @@ -44,7 +44,7 @@ export default class GameReader { lastPlayerPtr = 0; shouldReadLobby = false; exileCausesEnd = false; - is_64bit = false; + is64Bit = false; oldGameState = GameState.UNKNOWN; lastState: AmongUsState = {} as AmongUsState; amongUs: ProcessObject | null = null; @@ -100,10 +100,10 @@ export default class GameReader { meetingHud === 0 ? 0 : this.readMemory( - 'pointer', - meetingHud, - this.offsets.meetingHudCachePtr - ); + 'pointer', + meetingHud, + this.offsets.meetingHudCachePtr + ); const meetingHudState = meetingHud_cachePtr === 0 ? 4 @@ -135,12 +135,12 @@ export default class GameReader { state === GameState.MENU ? '' : this.IntToGameCode( - this.readMemory( - 'int32', - this.gameAssembly.modBaseAddr, - this.offsets.gameCode - ) - ); + this.readMemory( + 'int32', + this.gameAssembly.modBaseAddr, + this.offsets.gameCode + ) + ); const hostId = this.readMemory( 'uint32', @@ -179,6 +179,8 @@ export default class GameReader { let impostors = 0, crewmates = 0; + let commsSabotaged = false; + if (this.gameCode) { for (let i = 0; i < Math.min(playerCount, 100); i++) { const { address, last } = this.offsetAddress( @@ -192,7 +194,7 @@ export default class GameReader { ); const player = this.parsePlayer(address + last, playerData, clientId); - playerAddrPtr += this.is_64bit ? 8 : 4; + playerAddrPtr += this.is64Bit ? 8 : 4; if (!player) continue; players.push(player); @@ -207,6 +209,37 @@ export default class GameReader { if (player.isImpostor) impostors++; else crewmates++; } + + const shipPtr = this.readMemory('ptr', this.gameAssembly.modBaseAddr, this.offsets.shipStatus); + + const systemsPtr = this.readMemory('ptr', shipPtr, this.offsets.shipStatusSystems); + const map: MapType = this.readMemory('int32', shipPtr, this.offsets.shipStatusMap, MapType.UNKNOWN); + + if (systemsPtr !== 0 && (state === GameState.TASKS || state === GameState.DISCUSSION)) { + const entries = this.readMemory('ptr', systemsPtr + (this.is64Bit ? 0x18 : 0xc)); + const len = this.readMemory('uint32', entries + (this.is64Bit ? 0x18 : 0xc)); + + for (let i = 0; i < Math.min(len, 32); i++) { + const keyPtr = entries + ((this.is64Bit ? 0x20 : 0x10) + i * (this.is64Bit ? 0x18 : 0x10)); + const valPtr = keyPtr + (this.is64Bit ? 0x10 : 0xc); + const key = this.readMemory('int32', keyPtr); + if (key === 14) { + const value = this.readMemory('ptr', valPtr); + switch (map) { + case MapType.POLUS: + case MapType.THE_SKELD: { + commsSabotaged = + this.readMemory('uint32', value, this.offsets.commsSabotaged) === 1; + break; + } + case MapType.MIRA_HQ: { + commsSabotaged = + this.readMemory('uint32', value, this.offsets.miraCompletedCommsConsoles) < 2; + } + } + } + } + } } if ( @@ -241,6 +274,7 @@ export default class GameReader { isHost: (hostId && clientId && hostId === clientId) as boolean, hostId: hostId, clientId: clientId, + commsSabotaged }; const stateHasChanged = !equal(this.lastState, newState); if (stateHasChanged) { @@ -261,8 +295,8 @@ export default class GameReader { } initializeoffsets(): void { - this.is_64bit = this.isX64Version(); - this.offsets = this.is_64bit ? offsetStore.x64 : offsetStore.x86; + this.is64Bit = this.isX64Version(); + this.offsets = this.is64Bit ? offsetStore.x64 : offsetStore.x86; this.PlayerStruct = new Struct(); for (const member of this.offsets.player.struct) { if (member.type === 'SKIP' && member.skip) { @@ -322,14 +356,14 @@ export default class GameReader { readMemory( dataType: DataType, address: number, - offsets: number[], + offsets: number[] = [], defaultParam?: T ): T { if (!this.amongUs) return defaultParam as T; if (address === 0) return defaultParam as T; dataType = dataType == 'pointer' || dataType == 'ptr' - ? this.is_64bit + ? this.is64Bit ? 'uint64' : 'uint32' : dataType; @@ -343,12 +377,12 @@ export default class GameReader { offsets: number[] ): { address: number; last: number } { if (!this.amongUs) throw 'Among Us not open? Weird error'; - address = this.is_64bit ? address : address & 0xffffffff; + address = this.is64Bit ? address : address & 0xffffffff; for (let i = 0; i < offsets.length - 1; i++) { address = readMemoryRaw( this.amongUs.handle, address + offsets[i], - this.is_64bit ? 'uint64' : 'uint32' + this.is64Bit ? 'uint64' : 'uint32' ); if (address == 0) break; @@ -361,12 +395,12 @@ export default class GameReader { if (address === 0 || !this.amongUs) return ''; const length = readMemoryRaw( this.amongUs.handle, - address + (this.is_64bit ? 0x10 : 0x8), + address + (this.is64Bit ? 0x10 : 0x8), 'int' ); const buffer = readBuffer( this.amongUs.handle, - address + (this.is_64bit ? 0x14 : 0xc), + address + (this.is64Bit ? 0x14 : 0xc), length << 1 ); return buffer.toString('binary').replace(/\0/g, ''); @@ -392,7 +426,7 @@ export default class GameReader { this.gameAssembly.modBaseAddr, [instruction_location] ); - return this.is_64bit + return this.is64Bit ? offsetAddr + instruction_location + addressOffset : offsetAddr - this.gameAssembly.modBaseAddr; } @@ -422,7 +456,7 @@ export default class GameReader { const { data } = this.PlayerStruct.report(buffer, 0, {}); - if (this.is_64bit) { + if (this.is64Bit) { data.objectPtr = this.readMemory('pointer', ptr, [ this.PlayerStruct.getOffsetByName('objectPtr'), ]); diff --git a/src/main/index.ts b/src/main/index.ts index 2a20b97c..d502165f 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -10,6 +10,7 @@ import { overlayWindow as electronOverlayWindow } from 'electron-overlay-window' import { initializeIpcHandlers, initializeIpcListeners } from './ipc-handlers'; import { IpcRendererMessages } from '../common/ipc-messages'; import { ProgressInfo } from 'builder-util-runtime'; +import iohook from 'iohook'; const isDevelopment = process.env.NODE_ENV !== 'production'; @@ -206,6 +207,10 @@ if (!gotTheLock) { } }); + app.on('before-quit', () => { + iohook.stop(); + }) + app.on('activate', () => { // on macOS it is common to re-create a window even after all windows have been closed if (mainWindow === null) { diff --git a/src/main/offsetStore.ts b/src/main/offsetStore.ts index ba25de44..716d3dda 100644 --- a/src/main/offsetStore.ts +++ b/src/main/offsetStore.ts @@ -22,6 +22,11 @@ export interface IOffsets { gameCode: number[]; hostId: number[]; clientId: number[]; + shipStatus: number[]; + shipStatusSystems: number[]; + shipStatusMap: number[]; + miraCompletedCommsConsoles: number[]; + commsSabotaged: number[]; player: { localX: number[]; localY: number[]; @@ -53,6 +58,7 @@ export interface IOffsets { innerNetClient: ISignature; meetingHud: ISignature; gameData: ISignature; + shipStatus: ISignature; }; } @@ -70,6 +76,11 @@ export default { playerCount: [0x18], playerAddrPtr: 0x20, exiledPlayerId: [0xff, 0x21d03e0, 0xb8, 0, 0xe0, 0x10], + shipStatus: [0x21d0ce0, 0xb8, 0x0], + shipStatusSystems: [0xc0], + shipStatusMap: [0x154], + miraCompletedCommsConsoles: [0x18, 0x20], // OAMJKPNKGBM + commsSabotaged: [0x10], player: { struct: [ { type: 'SKIP', skip: 16, name: 'unused' }, @@ -119,6 +130,11 @@ export default { patternOffset: 3, addressOffset: 4, }, + shipStatus: { + sig: '48 8B 05 ? ? ? ? 48 8B 5C 24 ? 48 8B 6C 24 ? 48 8B 74 24 ? 48 8B 88 ? ? ? ? 48 89 39 48 83 C4 20 5F', + patternOffset: 3, + addressOffset: 4, + }, }, }, x86: { @@ -134,6 +150,11 @@ export default { playerCount: [0x0c], playerAddrPtr: 0x10, exiledPlayerId: [0xff, 0x1c573a4, 0x5c, 0, 0x94, 0x08], + shipStatus: [0x1c57cac, 0x5c, 0x0], + shipStatusSystems: [0x84], + shipStatusMap: [0xd4], + miraCompletedCommsConsoles: [0xc, 0x10], // OAMJKPNKGBM + commsSabotaged: [0x8], player: { struct: [ { type: 'SKIP', skip: 8, name: 'unused' }, @@ -178,6 +199,11 @@ export default { patternOffset: 2, addressOffset: 0, }, + shipStatus: { + sig: 'A1 ? ? ? ? 8B 40 5C 8B 00 85 C0 74 5A 8B 80 ? ? ? ? 85 C0 74 50 6A 00 6A 00', + patternOffset: 1, + addressOffset: 0, + }, }, }, } as IOffsetsStore; diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 365edbc4..bbaefd45 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -181,7 +181,8 @@ export default function App() { localLobbySettings: { maxDistance: 5.32, haunting: false, - hearImpostorsInVents: false + hearImpostorsInVents: false, + commsSabotage: true }, }); const lobbySettings = useReducer( diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index f50e2329..267461ab 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -86,6 +86,11 @@ function calculateVoiceAudio(state: AmongUsState, settings: ISettings, lobbySett case GameState.TASKS: gain.gain.value = 1; + // Mute all alive crewmates when comms is sabotaged + if (!me.isDead && lobbySettings.commsSabotage && state.commsSabotaged && !me.isImpostor) { + gain.gain.value = 0; + } + // Mute other players which are in a vent if (other.inVent && !lobbySettings.hearImpostorsInVents) { gain.gain.value = 0; @@ -96,8 +101,13 @@ function calculateVoiceAudio(state: AmongUsState, settings: ISettings, lobbySett gain.gain.value = 0; } - break; + // Haunting + if (!me.isDead && me.isImpostor && other.isDead && !other.isImpostor && lobbySettings.haunting) { + gain.gain.value = .075; + reverbGain.gain.value = 1; + } + break; case GameState.DISCUSSION: panPos = [0, 0]; gain.gain.value = 1; @@ -108,7 +118,6 @@ function calculateVoiceAudio(state: AmongUsState, settings: ISettings, lobbySett } break; - case GameState.UNKNOWN: default: gain.gain.value = 0; @@ -126,11 +135,6 @@ function calculateVoiceAudio(state: AmongUsState, settings: ISettings, lobbySett muffle.Q.value = 0; } - // Haunting - if (!me.isDead && me.isImpostor && other.isDead && !other.isImpostor && lobbySettings.haunting && state.gameState === GameState.TASKS) { - gain.gain.value = .075; - reverbGain.gain.value = 1; - } // Reset panning position if the setting is disabled if (!settings.enableSpatialAudio) { diff --git a/src/renderer/settings/Settings.tsx b/src/renderer/settings/Settings.tsx index 510fec18..8b8e1eee 100644 --- a/src/renderer/settings/Settings.tsx +++ b/src/renderer/settings/Settings.tsx @@ -228,12 +228,17 @@ const storeConfig: Store.Options = { hearImpostorsInVents: { type: 'boolean', default: false + }, + commsSabotage: { + type: 'boolean', + default: true } }, default: { maxDistance: 5.32, haunting: false, - hearImpostorsInVents: false + hearImpostorsInVents: false, + commsSabotage: true }, }, meetingOverlay: { @@ -658,6 +663,37 @@ const Settings: React.FC = function ({ control={} /> + + { + setSettings({ + type: 'setLobbySetting', + action: ['commsSabotage', checked], + }); + if (gameState?.isHost) { + setLobbySettings({ + type: 'setOne', + action: ['commsSabotage', checked], + }); + } + }} + control={} + /> +
Audio From c29b62af9e193d523ce5cfbee57c16b4c705969f Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 9 Jan 2021 13:59:50 -0800 Subject: [PATCH 81/88] [chore] Lint --- src/common/AmongUsState.ts | 2 +- src/common/ipc-messages.ts | 2 +- src/main/GameReader.ts | 80 +++++++++---- src/main/index.ts | 36 +++--- src/main/ipc-handlers.ts | 9 +- src/main/offsetStore.ts | 6 +- src/renderer/App.tsx | 47 +++++--- src/renderer/Avatar.tsx | 2 +- src/renderer/Footer.tsx | 4 +- src/renderer/Overlay.tsx | 182 ++++++++++++++++++----------- src/renderer/Voice.tsx | 70 ++++++++--- src/renderer/index.ts | 2 +- src/renderer/settings/Settings.tsx | 33 +++--- 13 files changed, 311 insertions(+), 164 deletions(-) diff --git a/src/common/AmongUsState.ts b/src/common/AmongUsState.ts index d6bed4f5..4fd3fc55 100644 --- a/src/common/AmongUsState.ts +++ b/src/common/AmongUsState.ts @@ -68,4 +68,4 @@ export interface VoiceState { otherDead: OtherTalking; socketClients: SocketClientMap; audioConnected: AudioConnected; -} \ No newline at end of file +} diff --git a/src/common/ipc-messages.ts b/src/common/ipc-messages.ts index e9c01e7b..2070237f 100644 --- a/src/common/ipc-messages.ts +++ b/src/common/ipc-messages.ts @@ -13,7 +13,7 @@ export enum IpcMessages { export enum IpcOverlayMessages { NOTIFY_GAME_STATE_CHANGED = 'NOTIFY_GAME_STATE_CHANGED', NOTIFY_VOICE_STATE_CHANGED = 'NOTIFY_VOICE_STATE_CHANGED', - NOTIFY_SETTINGS_CHANGED = 'NOTIFY_SETTINGS_CHANGED' + NOTIFY_SETTINGS_CHANGED = 'NOTIFY_SETTINGS_CHANGED', } // Renderer --> Main (sendSync/on) diff --git a/src/main/GameReader.ts b/src/main/GameReader.ts index 85a25b1e..3638a6ab 100644 --- a/src/main/GameReader.ts +++ b/src/main/GameReader.ts @@ -11,7 +11,12 @@ import { } from 'memoryjs'; import Struct from 'structron'; import { IpcRendererMessages } from '../common/ipc-messages'; -import { GameState, AmongUsState, Player, MapType } from '../common/AmongUsState'; +import { + GameState, + AmongUsState, + Player, + MapType, +} from '../common/AmongUsState'; import equal from 'deep-equal'; import offsetStore, { IOffsets } from './offsetStore'; import Errors from '../common/Errors'; @@ -78,7 +83,6 @@ export default class GameReader { } loop(): string | null { - try { this.checkProcessOpen(); } catch (e) { @@ -100,10 +104,10 @@ export default class GameReader { meetingHud === 0 ? 0 : this.readMemory( - 'pointer', - meetingHud, - this.offsets.meetingHudCachePtr - ); + 'pointer', + meetingHud, + this.offsets.meetingHudCachePtr + ); const meetingHudState = meetingHud_cachePtr === 0 ? 4 @@ -135,12 +139,12 @@ export default class GameReader { state === GameState.MENU ? '' : this.IntToGameCode( - this.readMemory( - 'int32', - this.gameAssembly.modBaseAddr, - this.offsets.gameCode - ) - ); + this.readMemory( + 'int32', + this.gameAssembly.modBaseAddr, + this.offsets.gameCode + ) + ); const hostId = this.readMemory( 'uint32', @@ -210,17 +214,41 @@ export default class GameReader { else crewmates++; } - const shipPtr = this.readMemory('ptr', this.gameAssembly.modBaseAddr, this.offsets.shipStatus); + const shipPtr = this.readMemory( + 'ptr', + this.gameAssembly.modBaseAddr, + this.offsets.shipStatus + ); - const systemsPtr = this.readMemory('ptr', shipPtr, this.offsets.shipStatusSystems); - const map: MapType = this.readMemory('int32', shipPtr, this.offsets.shipStatusMap, MapType.UNKNOWN); + const systemsPtr = this.readMemory( + 'ptr', + shipPtr, + this.offsets.shipStatusSystems + ); + const map: MapType = this.readMemory( + 'int32', + shipPtr, + this.offsets.shipStatusMap, + MapType.UNKNOWN + ); - if (systemsPtr !== 0 && (state === GameState.TASKS || state === GameState.DISCUSSION)) { - const entries = this.readMemory('ptr', systemsPtr + (this.is64Bit ? 0x18 : 0xc)); - const len = this.readMemory('uint32', entries + (this.is64Bit ? 0x18 : 0xc)); + if ( + systemsPtr !== 0 && + (state === GameState.TASKS || state === GameState.DISCUSSION) + ) { + const entries = this.readMemory( + 'ptr', + systemsPtr + (this.is64Bit ? 0x18 : 0xc) + ); + const len = this.readMemory( + 'uint32', + entries + (this.is64Bit ? 0x18 : 0xc) + ); for (let i = 0; i < Math.min(len, 32); i++) { - const keyPtr = entries + ((this.is64Bit ? 0x20 : 0x10) + i * (this.is64Bit ? 0x18 : 0x10)); + const keyPtr = + entries + + ((this.is64Bit ? 0x20 : 0x10) + i * (this.is64Bit ? 0x18 : 0x10)); const valPtr = keyPtr + (this.is64Bit ? 0x10 : 0xc); const key = this.readMemory('int32', keyPtr); if (key === 14) { @@ -229,12 +257,20 @@ export default class GameReader { case MapType.POLUS: case MapType.THE_SKELD: { commsSabotaged = - this.readMemory('uint32', value, this.offsets.commsSabotaged) === 1; + this.readMemory( + 'uint32', + value, + this.offsets.commsSabotaged + ) === 1; break; } case MapType.MIRA_HQ: { commsSabotaged = - this.readMemory('uint32', value, this.offsets.miraCompletedCommsConsoles) < 2; + this.readMemory( + 'uint32', + value, + this.offsets.miraCompletedCommsConsoles + ) < 2; } } } @@ -274,7 +310,7 @@ export default class GameReader { isHost: (hostId && clientId && hostId === clientId) as boolean, hostId: hostId, clientId: clientId, - commsSabotaged + commsSabotaged, }; const stateHasChanged = !equal(this.lastState, newState); if (stateHasChanged) { diff --git a/src/main/index.ts b/src/main/index.ts index d502165f..ba43d914 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -65,7 +65,7 @@ function createMainWindow() { protocol: 'file', query: { version: autoUpdater.currentVersion.version, - view: 'app' + view: 'app', }, slashes: true, }) @@ -78,7 +78,7 @@ function createMainWindow() { if (overlayWindow != null) { try { overlayWindow.close(); - } catch (_) { } + } catch (_) {} overlayWindow = null; } }); @@ -165,23 +165,27 @@ if (!gotTheLock) { height: 300, webPreferences: { nodeIntegration: true, - webSecurity: false + webSecurity: false, }, - ...electronOverlayWindow.WINDOW_OPTS + ...electronOverlayWindow.WINDOW_OPTS, }); if (isDevelopment) { - window.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}?version=${autoUpdater.currentVersion.version}&view=overlay`) + window.loadURL( + `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}?version=${autoUpdater.currentVersion.version}&view=overlay` + ); } else { - window.loadURL(formatUrl({ - pathname: joinPath(__dirname, 'index.html'), - protocol: 'file', - query: { - version: autoUpdater.currentVersion.version, - view: "overlay" - }, - slashes: true - })) + window.loadURL( + formatUrl({ + pathname: joinPath(__dirname, 'index.html'), + protocol: 'file', + query: { + version: autoUpdater.currentVersion.version, + view: 'overlay', + }, + slashes: true, + }) + ); } window.setIgnoreMouseEvents(true); electronOverlayWindow.attachTo(window, 'Among Us'); @@ -200,7 +204,7 @@ if (!gotTheLock) { // on macOS it is common for applications to stay open until the user explicitly quits if (process.platform !== 'darwin') { if (overlayWindow != null) { - overlayWindow.close() + overlayWindow.close(); overlayWindow = null; } app.quit(); @@ -209,7 +213,7 @@ if (!gotTheLock) { app.on('before-quit', () => { iohook.stop(); - }) + }); app.on('activate', () => { // on macOS it is common to re-create a window even after all windows have been closed diff --git a/src/main/ipc-handlers.ts b/src/main/ipc-handlers.ts index 2789c5eb..12973910 100644 --- a/src/main/ipc-handlers.ts +++ b/src/main/ipc-handlers.ts @@ -57,9 +57,12 @@ export const initializeIpcListeners = (overlayWindow: BrowserWindow): void => { app.quit(); }); - ipcMain.on(IpcMessages.SEND_TO_OVERLAY, (_, event: IpcOverlayMessages, ...args: unknown[]) => { - overlayWindow.webContents.send(event, ...args); - }); + ipcMain.on( + IpcMessages.SEND_TO_OVERLAY, + (_, event: IpcOverlayMessages, ...args: unknown[]) => { + overlayWindow.webContents.send(event, ...args); + } + ); }; // Handlers are async cross-process instructions, they should have a return value diff --git a/src/main/offsetStore.ts b/src/main/offsetStore.ts index 716d3dda..dfa2c2b0 100644 --- a/src/main/offsetStore.ts +++ b/src/main/offsetStore.ts @@ -131,7 +131,8 @@ export default { addressOffset: 4, }, shipStatus: { - sig: '48 8B 05 ? ? ? ? 48 8B 5C 24 ? 48 8B 6C 24 ? 48 8B 74 24 ? 48 8B 88 ? ? ? ? 48 89 39 48 83 C4 20 5F', + sig: + '48 8B 05 ? ? ? ? 48 8B 5C 24 ? 48 8B 6C 24 ? 48 8B 74 24 ? 48 8B 88 ? ? ? ? 48 89 39 48 83 C4 20 5F', patternOffset: 3, addressOffset: 4, }, @@ -200,7 +201,8 @@ export default { addressOffset: 0, }, shipStatus: { - sig: 'A1 ? ? ? ? 8B 40 5C 8B 00 85 C0 74 5A 8B 80 ? ? ? ? 85 C0 74 50 6A 00 6A 00', + sig: + 'A1 ? ? ? ? 8B 40 5C 8B 00 85 C0 74 5A 8B 80 ? ? ? ? 85 C0 74 50 6A 00 6A 00', patternOffset: 1, addressOffset: 0, }, diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index bbaefd45..7fbf67a7 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -126,15 +126,13 @@ class ErrorBoundary extends React.Component<{}, ErrorBoundaryState> { this.state = {}; } - - static getDerivedStateFromError(error: Error): ErrorBoundaryState { - // Update state so the next render will show the fallback UI. - return { error }; - } - + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + // Update state so the next render will show the fallback UI. + return { error }; + } componentDidCatch(error: Error, errorInfo: ErrorInfo) { - console.error("React Error: ", error, errorInfo); + console.error('React Error: ', error, errorInfo); } render() { @@ -144,11 +142,26 @@ class ErrorBoundary extends React.Component<{}, ErrorBoundaryState> { REACT ERROR - + {this.state.error.stack} - +
); } @@ -182,7 +195,7 @@ export default function App() { maxDistance: 5.32, haunting: false, hearImpostorsInVents: false, - commsSabotage: true + commsSabotage: true, }, }); const lobbySettings = useReducer( @@ -241,11 +254,19 @@ export default function App() { }, []); useEffect(() => { - ipcRenderer.send(IpcMessages.SEND_TO_OVERLAY, IpcOverlayMessages.NOTIFY_GAME_STATE_CHANGED, gameState); + ipcRenderer.send( + IpcMessages.SEND_TO_OVERLAY, + IpcOverlayMessages.NOTIFY_GAME_STATE_CHANGED, + gameState + ); }, [gameState]); useEffect(() => { - ipcRenderer.send(IpcMessages.SEND_TO_OVERLAY, IpcOverlayMessages.NOTIFY_SETTINGS_CHANGED, settings[0]); + ipcRenderer.send( + IpcMessages.SEND_TO_OVERLAY, + IpcOverlayMessages.NOTIFY_SETTINGS_CHANGED, + settings[0] + ); }, [settings]); let page; @@ -303,7 +324,7 @@ export default function App() { + )} diff --git a/src/renderer/Avatar.tsx b/src/renderer/Avatar.tsx index d991ca2a..e1fb0578 100644 --- a/src/renderer/Avatar.tsx +++ b/src/renderer/Avatar.tsx @@ -70,7 +70,7 @@ const Avatar: React.FC = function ({ player, size, connectionState, - style + style, }: AvatarProps) { const status = isAlive ? 'alive' : 'dead'; let image = players[status][player.colorId]; diff --git a/src/renderer/Footer.tsx b/src/renderer/Footer.tsx index 4d33be80..82857e99 100644 --- a/src/renderer/Footer.tsx +++ b/src/renderer/Footer.tsx @@ -19,8 +19,8 @@ const useStyles = makeStyles(() => ({ justifyContent: 'space-evenly', margin: 5, '&>svg': { - cursor: 'pointer' - } + cursor: 'pointer', + }, }, })); diff --git a/src/renderer/Overlay.tsx b/src/renderer/Overlay.tsx index b2bbe775..8ca9cc5d 100644 --- a/src/renderer/Overlay.tsx +++ b/src/renderer/Overlay.tsx @@ -1,10 +1,15 @@ import React, { useEffect, useMemo, useState } from 'react'; import { ipcRenderer } from 'electron'; -import { AmongUsState, GameState, VoiceState, OtherTalking } from '../common/AmongUsState'; +import { + AmongUsState, + GameState, + VoiceState, + OtherTalking, +} from '../common/AmongUsState'; import { IpcOverlayMessages } from '../common/ipc-messages'; import ReactDOM from 'react-dom'; import makeStyles from '@material-ui/core/styles/makeStyles'; -import './css/overlay.css' +import './css/overlay.css'; import Avatar from './Avatar'; import { ISettings } from '../common/ISettings'; @@ -17,7 +22,7 @@ const useStyles = makeStyles((theme) => ({ position: 'absolute', top: '50%', left: '50%', - transform: 'translate(-50%, -50%)' + transform: 'translate(-50%, -50%)', }, playerIcons: { width: '83.45%', @@ -27,12 +32,12 @@ const useStyles = makeStyles((theme) => ({ position: 'absolute', display: 'flex', '&>*:nth-child(odd)': { - marginRight: '1.4885%' + marginRight: '1.4885%', }, '&>*:nth-child(even)': { - marginLeft: '1.4885%' + marginLeft: '1.4885%', }, - flexWrap: 'wrap' + flexWrap: 'wrap', }, icon: { width: '48.51%', @@ -40,12 +45,10 @@ const useStyles = makeStyles((theme) => ({ borderRadius: ({ hudHeight }: UseStylesProps) => hudHeight / 100, transition: 'opacity .1s linear', marginBottom: '2.25%', - boxSizing: 'border-box' - } + boxSizing: 'border-box', + }, })); - - function useWindowSize() { const [windowSize, setWindowSize] = useState<[number, number]>([0, 0]); @@ -62,31 +65,40 @@ function useWindowSize() { } const playerColors = [ - ['#C51111', '#7A0838',], - ['#132ED1', '#09158E',], - ['#117F2D', '#0A4D2E',], - ['#ED54BA', '#AB2BAD',], - ['#EF7D0D', '#B33E15',], - ['#F5F557', '#C38823',], - ['#3F474E', '#1E1F26',], - ['#8394BF', '#8394BF',], - ['#6B2FBB', '#3B177C',], - ['#71491E', '#5E2615',], - ['#38FEDC', '#24A8BE',], - ['#50EF39', '#15A742',] + ['#C51111', '#7A0838'], + ['#132ED1', '#09158E'], + ['#117F2D', '#0A4D2E'], + ['#ED54BA', '#AB2BAD'], + ['#EF7D0D', '#B33E15'], + ['#F5F557', '#C38823'], + ['#3F474E', '#1E1F26'], + ['#8394BF', '#8394BF'], + ['#6B2FBB', '#3B177C'], + ['#71491E', '#5E2615'], + ['#38FEDC', '#24A8BE'], + ['#50EF39', '#15A742'], ]; const iPadRatio = 854 / 579; export default function Overlay() { - const [gameState, setGameState] = useState(undefined as unknown as AmongUsState); - const [voiceState, setVoiceState] = useState(undefined as unknown as VoiceState); - const [settings, setSettings] = useState(undefined as unknown as ISettings); + const [gameState, setGameState] = useState( + (undefined as unknown) as AmongUsState + ); + const [voiceState, setVoiceState] = useState( + (undefined as unknown) as VoiceState + ); + const [settings, setSettings] = useState( + (undefined as unknown) as ISettings + ); useEffect(() => { const onState = (_: Electron.IpcRendererEvent, newState: AmongUsState) => { setGameState(newState); }; - const onVoiceState = (_: Electron.IpcRendererEvent, newState: VoiceState) => { + const onVoiceState = ( + _: Electron.IpcRendererEvent, + newState: VoiceState + ) => { setVoiceState(newState); }; const onSettings = (_: Electron.IpcRendererEvent, newState: ISettings) => { @@ -97,22 +109,30 @@ export default function Overlay() { ipcRenderer.on(IpcOverlayMessages.NOTIFY_SETTINGS_CHANGED, onSettings); return () => { ipcRenderer.off(IpcOverlayMessages.NOTIFY_GAME_STATE_CHANGED, onState); - ipcRenderer.off(IpcOverlayMessages.NOTIFY_VOICE_STATE_CHANGED, onVoiceState); + ipcRenderer.off( + IpcOverlayMessages.NOTIFY_VOICE_STATE_CHANGED, + onVoiceState + ); ipcRenderer.off(IpcOverlayMessages.NOTIFY_SETTINGS_CHANGED, onSettings); - } + }; }, []); if (!settings || !voiceState || !gameState) return null; return ( <> - { - settings.meetingOverlay && - - } - { - settings.overlayPosition !== 'hidden' && - - } + {settings.meetingOverlay && ( + + )} + {settings.overlayPosition !== 'hidden' && ( + + )} ); } @@ -120,7 +140,7 @@ export default function Overlay() { interface AvatarOverlayProps { voiceState: VoiceState; gameState: AmongUsState; - position: ISettings["overlayPosition"] + position: ISettings['overlayPosition']; } const useOverlayStyles = makeStyles((theme) => ({ @@ -131,16 +151,20 @@ const useOverlayStyles = makeStyles((theme) => ({ padding: theme.spacing(2), '&>*': { marginTop: 4, - marginBottom: 4 - } - } + marginBottom: 4, + }, + }, })); -const AvatarOverlay: React.FC = ({ voiceState, gameState, position }: AvatarOverlayProps) => { +const AvatarOverlay: React.FC = ({ + voiceState, + gameState, + position, +}: AvatarOverlayProps) => { if (!gameState.players) return null; const classes = useOverlayStyles({ position }); const avatars: JSX.Element[] = []; - gameState.players.forEach(player => { + gameState.players.forEach((player) => { if (!voiceState.otherTalking[player.id]) return; const peer = voiceState.playerSocketIds[player.id]; const connected = Object.values(voiceState.socketClients) @@ -163,19 +187,22 @@ const AvatarOverlay: React.FC = ({ voiceState, gameState, po }); if (avatars.length === 0) return null; return ( -
+
{avatars}
- ) + ); }; interface MeetingHudProps { @@ -183,10 +210,14 @@ interface MeetingHudProps { gameState: AmongUsState; } -const MeetingHud: React.FC = ({ otherTalking, gameState }: MeetingHudProps) => { +const MeetingHud: React.FC = ({ + otherTalking, + gameState, +}: MeetingHudProps) => { const [width, height] = useWindowSize(); - let hudWidth = 0, hudHeight = 0; + let hudWidth = 0, + hudHeight = 0; if (width / (height * 0.96) > iPadRatio) { hudHeight = height * 0.96; hudWidth = hudHeight * iPadRatio; @@ -206,31 +237,44 @@ const MeetingHud: React.FC = ({ otherTalking, gameState }: Meet return -1000; } return a.id - b.id; - }) + }); }, [gameState.players]); if (!players || gameState.gameState !== GameState.DISCUSSION) return null; const overlays = gameState.players.map((player) => { return ( -
+ boxShadow: `0 0 ${hudHeight / 100}px ${hudHeight / 100}px ${ + playerColors[player.colorId][0] + }`, + }} + /> ); }); while (overlays.length < 10) { - overlays.push(
); + overlays.push( +
+ ); } - return
-
- {overlays} + return ( +
+
{overlays}
-
; -} + ); +}; -ReactDOM.render(, document.getElementById('app')); \ No newline at end of file +ReactDOM.render(, document.getElementById('app')); diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index 267461ab..ecc47ad9 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -6,12 +6,25 @@ import { LobbySettingsContext, SettingsContext, } from './contexts'; -import { AmongUsState, AudioConnected, Client, GameState, OtherTalking, Player, SocketClientMap, VoiceState } from '../common/AmongUsState'; +import { + AmongUsState, + AudioConnected, + Client, + GameState, + OtherTalking, + Player, + SocketClientMap, + VoiceState, +} from '../common/AmongUsState'; import Peer from 'simple-peer'; import { ipcRenderer } from 'electron'; import VAD from './vad'; import { ILobbySettings, ISettings } from '../common/ISettings'; -import { IpcMessages, IpcOverlayMessages, IpcRendererMessages } from '../common/ipc-messages'; +import { + IpcMessages, + IpcOverlayMessages, + IpcRendererMessages, +} from '../common/ipc-messages'; import Typography from '@material-ui/core/Typography'; import Grid from '@material-ui/core/Grid'; import makeStyles from '@material-ui/core/styles/makeStyles'; @@ -54,7 +67,6 @@ interface AudioElements { [peer: string]: AudioNodes; } - interface ConnectionStuff { socket?: typeof Socket; stream?: MediaStream; @@ -67,7 +79,14 @@ interface SocketError { message?: string; } -function calculateVoiceAudio(state: AmongUsState, settings: ISettings, lobbySettings: ILobbySettings, me: Player, other: Player, audio: AudioNodes): void { +function calculateVoiceAudio( + state: AmongUsState, + settings: ISettings, + lobbySettings: ILobbySettings, + me: Player, + other: Player, + audio: AudioNodes +): void { const { pan, gain, muffle, reverbGain } = audio; const audioContext = pan.context; let panPos = [other.x - me.x, other.y - me.y]; @@ -87,7 +106,12 @@ function calculateVoiceAudio(state: AmongUsState, settings: ISettings, lobbySett gain.gain.value = 1; // Mute all alive crewmates when comms is sabotaged - if (!me.isDead && lobbySettings.commsSabotage && state.commsSabotaged && !me.isImpostor) { + if ( + !me.isDead && + lobbySettings.commsSabotage && + state.commsSabotaged && + !me.isImpostor + ) { gain.gain.value = 0; } @@ -102,8 +126,14 @@ function calculateVoiceAudio(state: AmongUsState, settings: ISettings, lobbySett } // Haunting - if (!me.isDead && me.isImpostor && other.isDead && !other.isImpostor && lobbySettings.haunting) { - gain.gain.value = .075; + if ( + !me.isDead && + me.isImpostor && + other.isDead && + !other.isImpostor && + lobbySettings.haunting + ) { + gain.gain.value = 0.075; reverbGain.gain.value = 1; } @@ -128,14 +158,12 @@ function calculateVoiceAudio(state: AmongUsState, settings: ISettings, lobbySett if (me.inVent || other.inVent) { muffle.frequency.value = 1200; muffle.Q.value = 20; - if (gain.gain.value === 1) - gain.gain.value = 0.7; // Too loud at 1 + if (gain.gain.value === 1) gain.gain.value = 0.7; // Too loud at 1 } else { muffle.frequency.value = 20000; muffle.Q.value = 0; } - // Reset panning position if the setting is disabled if (!settings.enableSpatialAudio) { panPos = [0, 0]; @@ -230,8 +258,7 @@ const Voice: React.FC = function ({ const lobbySettingsRef = useRef(lobbySettings); const gameState = useContext(GameStateContext); let displayedLobbyCode = ''; - if (gameState) - displayedLobbyCode = gameState.lobbyCode; + if (gameState) displayedLobbyCode = gameState.lobbyCode; if (displayedLobbyCode !== 'MENU' && settings.hideCode) displayedLobbyCode = 'LOBBY'; const [talking, setTalking] = useState(false); @@ -547,7 +574,15 @@ const Voice: React.FC = function ({ talking && gain.gain.value > 0, })); }; - audioElements.current[peer] = { element: audio, gain, pan, muffle, reverb, reverbGain, compressor }; + audioElements.current[peer] = { + element: audio, + gain, + pan, + muffle, + reverb, + reverbGain, + compressor, + }; }); connection.on('signal', (data) => { socket.emit('signal', { @@ -741,14 +776,17 @@ const Voice: React.FC = function ({ // Pass voice state to overlay useEffect(() => { - ipcRenderer.send(IpcMessages.SEND_TO_OVERLAY, IpcOverlayMessages.NOTIFY_VOICE_STATE_CHANGED, + ipcRenderer.send( + IpcMessages.SEND_TO_OVERLAY, + IpcOverlayMessages.NOTIFY_VOICE_STATE_CHANGED, { otherTalking, playerSocketIds, otherDead, socketClients, - audioConnected - } as VoiceState); + audioConnected, + } as VoiceState + ); }, [otherTalking, playerSocketIds, otherDead, socketClients, audioConnected]); return ( diff --git a/src/renderer/index.ts b/src/renderer/index.ts index 8f278ce3..5329ebc4 100644 --- a/src/renderer/index.ts +++ b/src/renderer/index.ts @@ -6,4 +6,4 @@ if (typeof window !== 'undefined' && window.location) { } else { import('./Overlay'); } -} \ No newline at end of file +} diff --git a/src/renderer/settings/Settings.tsx b/src/renderer/settings/Settings.tsx index 8b8e1eee..a4ac20a8 100644 --- a/src/renderer/settings/Settings.tsx +++ b/src/renderer/settings/Settings.tsx @@ -227,29 +227,29 @@ const storeConfig: Store.Options = { }, hearImpostorsInVents: { type: 'boolean', - default: false + default: false, }, commsSabotage: { type: 'boolean', - default: true - } + default: true, + }, }, default: { maxDistance: 5.32, haunting: false, hearImpostorsInVents: false, - commsSabotage: true + commsSabotage: true, }, }, meetingOverlay: { type: 'boolean', - default: true + default: true, }, overlayPosition: { type: 'string', enum: ['left', 'right', 'hidden'], - default: 'right' - } + default: 'right', + }, }, }; @@ -350,11 +350,7 @@ const URLInput: React.FC = function ({ return ( <> - setOpen(false)}> @@ -837,11 +833,14 @@ const Settings: React.FC = function ({ }); }} > - {(storeConfig.schema?.overlayPosition?.enum as string[]).map((position) => ( - - ))} + {(storeConfig.schema?.overlayPosition?.enum as string[]).map( + (position) => ( + + ) + )} Date: Sat, 9 Jan 2021 14:18:09 -0800 Subject: [PATCH 82/88] [chore] lint --- src/main/index.ts | 84 ++++++++++++++++++------------------ src/renderer/App.tsx | 93 ++++++++++++++++++++++------------------ src/renderer/Overlay.tsx | 8 ++-- 3 files changed, 99 insertions(+), 86 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index ba43d914..01acc139 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -78,7 +78,9 @@ function createMainWindow() { if (overlayWindow != null) { try { overlayWindow.close(); - } catch (_) {} + } catch (_) { + console.error(_); + } overlayWindow = null; } }); @@ -93,6 +95,46 @@ function createMainWindow() { return window; } +function createOverlay() { + const window = new BrowserWindow({ + width: 400, + height: 300, + webPreferences: { + nodeIntegration: true, + webSecurity: false, + }, + ...electronOverlayWindow.WINDOW_OPTS, + }); + + if (isDevelopment) { + window.loadURL( + `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}?version=${autoUpdater.currentVersion.version}&view=overlay` + ); + } else { + window.loadURL( + formatUrl({ + pathname: joinPath(__dirname, 'index.html'), + protocol: 'file', + query: { + version: autoUpdater.currentVersion.version, + view: 'overlay', + }, + slashes: true, + }) + ); + } + window.setIgnoreMouseEvents(true); + electronOverlayWindow.attachTo(window, 'Among Us'); + + if (isDevelopment) { + // Force devtools into detached mode otherwise they are unusable + window.webContents.openDevTools({ + mode: 'detach', + }); + } + return window; +} + const gotTheLock = app.requestSingleInstanceLock(); if (!gotTheLock) { app.quit(); @@ -159,46 +201,6 @@ if (!gotTheLock) { } }); - function createOverlay() { - const window = new BrowserWindow({ - width: 400, - height: 300, - webPreferences: { - nodeIntegration: true, - webSecurity: false, - }, - ...electronOverlayWindow.WINDOW_OPTS, - }); - - if (isDevelopment) { - window.loadURL( - `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}?version=${autoUpdater.currentVersion.version}&view=overlay` - ); - } else { - window.loadURL( - formatUrl({ - pathname: joinPath(__dirname, 'index.html'), - protocol: 'file', - query: { - version: autoUpdater.currentVersion.version, - view: 'overlay', - }, - slashes: true, - }) - ); - } - window.setIgnoreMouseEvents(true); - electronOverlayWindow.attachTo(window, 'Among Us'); - - if (isDevelopment) { - // Force devtools into detached mode otherwise they are unusable - window.webContents.openDevTools({ - mode: 'detach', - }); - } - return window; - } - // quit application when all windows are closed app.on('window-all-closed', () => { // on macOS it is common for applications to stay open until the user explicitly quits diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 7fbf67a7..a6903375 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1,6 +1,7 @@ import React, { Dispatch, ErrorInfo, + ReactChild, SetStateAction, useEffect, useReducer, @@ -116,12 +117,18 @@ enum AppState { VOICE, } +interface ErrorBoundaryProps { + children: ReactChild; +} interface ErrorBoundaryState { error?: Error; } -class ErrorBoundary extends React.Component<{}, ErrorBoundaryState> { - constructor(props: {}) { +class ErrorBoundary extends React.Component< + ErrorBoundaryProps, + ErrorBoundaryState +> { + constructor(props: ErrorBoundaryProps) { super(props); this.state = {}; } @@ -135,7 +142,7 @@ class ErrorBoundary extends React.Component<{}, ErrorBoundaryState> { console.error('React Error: ', error, errorInfo); } - render() { + render(): ReactChild { if (this.state.error) { return (
@@ -170,7 +177,7 @@ class ErrorBoundary extends React.Component<{}, ErrorBoundaryState> { } } -export default function App() { +const App: React.FC = function () { const [state, setState] = useState(AppState.MENU); const [gameState, setGameState] = useState({} as AmongUsState); const [settingsOpen, setSettingsOpen] = useState(false); @@ -289,52 +296,54 @@ export default function App() { setSettingsOpen={setSettingsOpen} /> - setSettingsOpen(false)} - /> - - Updating... - - {(updaterState.state === 'downloading' || - updaterState.state === 'downloaded') && - updaterState.progress && ( - <> - - - {prettyBytes(updaterState.progress.transferred)} /{' '} - {prettyBytes(updaterState.progress.total)} - - + <> + setSettingsOpen(false)} + /> + + Updating... + + {(updaterState.state === 'downloading' || + updaterState.state === 'downloaded') && + updaterState.progress && ( + <> + + + {prettyBytes(updaterState.progress.transferred)} /{' '} + {prettyBytes(updaterState.progress.total)} + + + )} + {updaterState.state === 'error' && ( + + {updaterState.error} + )} + {updaterState.state === 'error' && ( - - {updaterState.error} - + + + )} - - {updaterState.state === 'error' && ( - - - - )} - - {page} +
+ {page} + ); -} +}; ReactDOM.render(, document.getElementById('app')); diff --git a/src/renderer/Overlay.tsx b/src/renderer/Overlay.tsx index 8ca9cc5d..036b03ea 100644 --- a/src/renderer/Overlay.tsx +++ b/src/renderer/Overlay.tsx @@ -17,7 +17,7 @@ interface UseStylesProps { hudHeight: number; } -const useStyles = makeStyles((theme) => ({ +const useStyles = makeStyles(() => ({ meetingHud: { position: 'absolute', top: '50%', @@ -81,7 +81,7 @@ const playerColors = [ const iPadRatio = 854 / 579; -export default function Overlay() { +const Overlay: React.FC = function () { const [gameState, setGameState] = useState( (undefined as unknown) as AmongUsState ); @@ -135,7 +135,7 @@ export default function Overlay() { )} ); -} +}; interface AvatarOverlayProps { voiceState: VoiceState; @@ -278,3 +278,5 @@ const MeetingHud: React.FC = ({ }; ReactDOM.render(, document.getElementById('app')); + +export default Overlay; From c9eed5a22b9689a77d3e7564beca1701b7d29414 Mon Sep 17 00:00:00 2001 From: ottomated <31470743+ottomated@users.noreply.github.com> Date: Sat, 9 Jan 2021 15:14:43 -0800 Subject: [PATCH 83/88] Remove title from issue template --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 155cf322..aac57a8b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,7 +1,7 @@ --- name: Bug report about: Create a report to help us improve -title: '[BUG REPORT]' +title: '' labels: 'bug' assignees: '' --- From 33619a8e5d6d526d23937f914268d4df24a54ece Mon Sep 17 00:00:00 2001 From: ottomated <31470743+ottomated@users.noreply.github.com> Date: Sat, 9 Jan 2021 15:15:19 -0800 Subject: [PATCH 84/88] Remove title from issue template --- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index fef99b67..eff3df63 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,7 +1,7 @@ --- name: Feature request about: Suggest an idea for this project -title: '[FEATURE]' +title: '' labels: 'enhancement' assignees: '' --- From 99b80a2b9dca9c8c3b9e0b898e08a8079861567a Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 9 Jan 2021 18:31:21 -0800 Subject: [PATCH 85/88] fix issues caused by disabling spatial audio --- src/renderer/Voice.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index ecc47ad9..09d82f14 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -164,11 +164,6 @@ function calculateVoiceAudio( muffle.Q.value = 0; } - // Reset panning position if the setting is disabled - if (!settings.enableSpatialAudio) { - panPos = [0, 0]; - } - // Clamp panning position if (isNaN(panPos[0])) panPos[0] = 999; if (isNaN(panPos[1])) panPos[1] = 999; @@ -177,10 +172,15 @@ function calculateVoiceAudio( panPos[1] = Math.min(Math.max(panPos[1], -999), 999); // Mute players if distancte between two players is too big - if (Math.pow(panPos[0], 2) + Math.pow(panPos[1], 2) > 7 * 7) { + if (Math.pow(panPos[0], 2) + Math.pow(panPos[1], 2) > pan.maxDistance * pan.maxDistance) { gain.gain.value = 0; } + // Reset panning position if the setting is disabled + if (!settings.enableSpatialAudio) { + panPos = [0, 0]; + } + // Apply position's to PanNode pan.positionX.setValueAtTime(panPos[0], audioContext.currentTime); pan.positionY.setValueAtTime(panPos[1], audioContext.currentTime); From dc6e9706c91fcdc73556c260de7df51fb68bcfbb Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 9 Jan 2021 18:31:40 -0800 Subject: [PATCH 86/88] typo --- src/renderer/Voice.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index 09d82f14..d57c6c19 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -171,7 +171,7 @@ function calculateVoiceAudio( panPos[0] = Math.min(Math.max(panPos[0], -999), 999); panPos[1] = Math.min(Math.max(panPos[1], -999), 999); - // Mute players if distancte between two players is too big + // Mute players if distance between two players is too big if (Math.pow(panPos[0], 2) + Math.pow(panPos[1], 2) > pan.maxDistance * pan.maxDistance) { gain.gain.value = 0; } From 6e0c4ab04a0e030441f7e6a43b6b311e27ea3b00 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 9 Jan 2021 18:39:25 -0800 Subject: [PATCH 87/88] Fix issues with disabling spatial audio and javascript errors --- .eslintrc.yml | 3 +++ src/main/hook.ts | 32 ++++++++++++++++++++++++-------- src/renderer/Voice.tsx | 5 ++++- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 8c4eb172..538a8bb3 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -21,6 +21,9 @@ settings: react: version: 'detect' rules: + no-empty: + - error + - allowEmptyCatch: true linebreak-style: - error - unix diff --git a/src/main/hook.ts b/src/main/hook.ts index 378ce835..b4d6f4e1 100644 --- a/src/main/hook.ts +++ b/src/main/hook.ts @@ -44,22 +44,30 @@ ipcMain.handle(IpcHandlerMessages.START_HOOK, async (event) => { iohook.on('keydown', (ev: IOHookEvent) => { const shortcutKey = store.get('pushToTalkShortcut'); if (!isMouseButton(shortcutKey) && keyCodeMatches(shortcutKey as K, ev)) { - event.sender.send(IpcRendererMessages.PUSH_TO_TALK, true); + try { + event.sender.send(IpcRendererMessages.PUSH_TO_TALK, true); + } catch (_) {} } }); iohook.on('keyup', (ev: IOHookEvent) => { const shortcutKey = store.get('pushToTalkShortcut'); if (!isMouseButton(shortcutKey) && keyCodeMatches(shortcutKey as K, ev)) { - event.sender.send(IpcRendererMessages.PUSH_TO_TALK, false); + try { + event.sender.send(IpcRendererMessages.PUSH_TO_TALK, false); + } catch (_) {} } if ( !isMouseButton(store.get('deafenShortcut')) && keyCodeMatches(store.get('deafenShortcut') as K, ev) ) { - event.sender.send(IpcRendererMessages.TOGGLE_DEAFEN); + try { + event.sender.send(IpcRendererMessages.TOGGLE_DEAFEN); + } catch (_) {} } if (keyCodeMatches(store.get('muteShortcut', 'RAlt') as K, ev)) { - event.sender.send(IpcRendererMessages.TOGGLE_MUTE); + try { + event.sender.send(IpcRendererMessages.TOGGLE_MUTE); + } catch (_) {} } }); @@ -70,7 +78,9 @@ ipcMain.handle(IpcHandlerMessages.START_HOOK, async (event) => { isMouseButton(shortcutMouse) && mouseClickMatches(shortcutMouse as M, ev) ) { - event.sender.send(IpcRendererMessages.PUSH_TO_TALK, true); + try { + event.sender.send(IpcRendererMessages.PUSH_TO_TALK, true); + } catch (_) {} } }); iohook.on('mouseup', (ev: IOHookEvent) => { @@ -79,19 +89,25 @@ ipcMain.handle(IpcHandlerMessages.START_HOOK, async (event) => { isMouseButton(shortcutMouse) && mouseClickMatches(shortcutMouse as M, ev) ) { - event.sender.send(IpcRendererMessages.PUSH_TO_TALK, false); + try { + event.sender.send(IpcRendererMessages.PUSH_TO_TALK, false); + } catch (_) {} } if ( isMouseButton(store.get('deafenShortcut')) && mouseClickMatches(store.get('deafenShortcut') as M, ev) ) { - event.sender.send(IpcRendererMessages.TOGGLE_DEAFEN); + try { + event.sender.send(IpcRendererMessages.TOGGLE_DEAFEN); + } catch (_) {} } if ( isMouseButton(store.get('muteShortcut', 'RAlt')) && mouseClickMatches(store.get('muteShortcut', 'RAlt') as M, ev) ) { - event.sender.send(IpcRendererMessages.TOGGLE_MUTE); + try { + event.sender.send(IpcRendererMessages.TOGGLE_MUTE); + } catch (_) {} } }); diff --git a/src/renderer/Voice.tsx b/src/renderer/Voice.tsx index d57c6c19..77278291 100644 --- a/src/renderer/Voice.tsx +++ b/src/renderer/Voice.tsx @@ -172,7 +172,10 @@ function calculateVoiceAudio( panPos[1] = Math.min(Math.max(panPos[1], -999), 999); // Mute players if distance between two players is too big - if (Math.pow(panPos[0], 2) + Math.pow(panPos[1], 2) > pan.maxDistance * pan.maxDistance) { + if ( + Math.pow(panPos[0], 2) + Math.pow(panPos[1], 2) > + pan.maxDistance * pan.maxDistance + ) { gain.gain.value = 0; } From 46d177bbd0eadda27a3668cd42d1cc79b2046018 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Sat, 9 Jan 2021 18:39:32 -0800 Subject: [PATCH 88/88] v2.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 125076f5..99566e80 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "crewlink", - "version": "2.0.0", + "version": "2.0.1", "license": "GPL-3.0-or-later", "description": "Free, open, Among Us proximity voice chat", "repository": {