From 98d9a8dbe0e4bfebcec877bcf97eb3a1f0b4a684 Mon Sep 17 00:00:00 2001 From: Ryan Shavell Date: Sun, 6 Dec 2020 21:19:58 -0500 Subject: [PATCH 01/67] 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/67] 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 = "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"; + // 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/67] 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/67] 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 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 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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/67] 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 7efcc9398952e40de8cd2bc93a75621aa618cad6 Mon Sep 17 00:00:00 2001 From: Brian Luo Date: Sat, 12 Dec 2020 17:40:42 -0800 Subject: [PATCH 16/67] 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 17/67] 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 18/67] 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 19/67] 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 20/67] 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 698b1b69497edcbd5063c0a58ef0f6a673bf3933 Mon Sep 17 00:00:00 2001 From: Brian Luo Date: Mon, 14 Dec 2020 20:02:13 -0800 Subject: [PATCH 21/67] 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 22/67] 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 23/67] - 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 24/67] - 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 25/67] 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 26/67] 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 27/67] 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 28/67] 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 29/67] - 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 30/67] 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 31/67] 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 32/67] 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 7b871e59447eed22c6a6f6d7b538b2d9e1e242d9 Mon Sep 17 00:00:00 2001 From: OhMyGuus Date: Sun, 27 Dec 2020 20:04:59 +0100 Subject: [PATCH 33/67] 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 34/67] 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 35/67] 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 9d2cc566f495be405b78b04746cd19ea189ff35c Mon Sep 17 00:00:00 2001 From: OhMyGuus Date: Mon, 28 Dec 2020 20:16:14 +0100 Subject: [PATCH 36/67] - 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 4e8e2912b5e11e9735606c73187380d7c1449c77 Mon Sep 17 00:00:00 2001 From: Ottomated Date: Tue, 29 Dec 2020 22:00:13 -0800 Subject: [PATCH 37/67] 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 38/67] 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 39/67] 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 40/67] 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 41/67] 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 42/67] 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 43/67] 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 44/67] 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 48/67] 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 49/67] 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 50/67] 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 51/67] 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 52/67] 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 53/67] 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 54/67] 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 55/67] [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 56/67] [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 57/67] 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 58/67] 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 59/67] 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 60/67] 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 61/67] 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 62/67] 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": { From 09fa0f8dc7d008bfc25a1e3bb181171c54dcfab4 Mon Sep 17 00:00:00 2001 From: TheGreatMcPain Date: Sat, 16 Jan 2021 20:08:57 -0600 Subject: [PATCH 63/67] Disable hardware acceleration and add timeout before window creation. This allows the overlay to properly set transparency under Linux. The overlay still doesn't seem to work though. --- src/main/index.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index c3c9be6e..a343db4e 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -224,11 +224,18 @@ if (!gotTheLock) { } }); + app.disableHardwareAcceleration(); + // create main BrowserWindow when electron is ready app.whenReady().then(() => { - mainWindow = createMainWindow(); - overlayWindow = createOverlay(); - initializeIpcListeners(overlayWindow); - initializeIpcHandlers(); + setTimeout( + function() { + mainWindow = createMainWindow(); + overlayWindow = createOverlay(); + initializeIpcListeners(overlayWindow); + initializeIpcHandlers(); + }, + process.platform === 'linux' ? 1000 : 0 + ) }); } From 32c6fc951b372115089cb17a5a3de1e23f48cce3 Mon Sep 17 00:00:00 2001 From: TheGreatMcPain Date: Sat, 16 Jan 2021 20:25:09 -0600 Subject: [PATCH 64/67] Temporarily use TheGreatMcPain's fork of memoryjs. At least until that memoryjs pull-request gets merged into zbanks' fork. --- package.json | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 349a5fd4..95efdfd2 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "electron-updater": "^4.3.5", "electron-window-state": "^5.0.3", "iohook": "git://github.com/zbanks/iohook.git#0.7.2_zbanks", - "memoryjs": "https://github.com/zbanks/memoryjs.git", + "memoryjs": "https://github.com/TheGreatMcPain/memoryjs.git", "pretty-bytes": "^5.5.0", "react": "^17.0.1", "react-dom": "^17.0.1", diff --git a/yarn.lock b/yarn.lock index 23a22f3b..4c772c04 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7266,9 +7266,9 @@ memory-fs@^0.5.0: errno "^0.1.3" readable-stream "^2.0.1" -"memoryjs@https://github.com/zbanks/memoryjs.git": +"memoryjs@https://github.com/TheGreatMcPain/memoryjs.git": version "0.0.1" - resolved "https://github.com/zbanks/memoryjs.git#45975c05f0606269bd566bbf059ef14f0cec94e4" + resolved "https://github.com/TheGreatMcPain/memoryjs.git#b2f22662666e51116b2640dd8359c203d87402f0" dependencies: bindings "^1.5.0" concentrate "^0.2.3" From eb28b9fa66a1be85a6a7efa5cc946979005cb396 Mon Sep 17 00:00:00 2001 From: TheGreatMcPain Date: Sun, 17 Jan 2021 14:27:35 -0600 Subject: [PATCH 65/67] Add '--disable-overlay' cmdline option. The option will prevent the overlay from attaching to Among Us while also disabling the workarounds that got the overlay's transparency working. This for those who have trouble with CrewLink's overlay since the 'hidden' overlay setting within CrewLink doesn't completely stop the overlay from attaching to Among Us. --- src/main/index.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index a343db4e..736781a4 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -16,6 +16,16 @@ const isDevelopment = process.env.NODE_ENV !== 'production'; let mainWindow: BrowserWindow | null = null; let overlayWindow: BrowserWindow | null = null; +let disableOverlay: boolean = false; +let setTimeoutValue: number = 0; + +if (app.commandLine.hasSwitch('disable-overlay')) { + disableOverlay = true; +} else { + if (process.platform === 'linux') { + setTimeoutValue = 1000; + } +} app.commandLine.appendSwitch('disable-pinch'); @@ -124,7 +134,9 @@ function createOverlay() { ); } window.setIgnoreMouseEvents(true); - electronOverlayWindow.attachTo(window, 'Among Us'); + if (!disableOverlay) { + electronOverlayWindow.attachTo(window, 'Among Us'); + } if (isDevelopment) { // Force devtools into detached mode otherwise they are unusable @@ -224,7 +236,10 @@ if (!gotTheLock) { } }); - app.disableHardwareAcceleration(); + // Only needed to get proper transparency for the overlay under Linux + if (!disableOverlay) { + app.disableHardwareAcceleration(); + } // create main BrowserWindow when electron is ready app.whenReady().then(() => { @@ -235,7 +250,7 @@ if (!gotTheLock) { initializeIpcListeners(overlayWindow); initializeIpcHandlers(); }, - process.platform === 'linux' ? 1000 : 0 + setTimeoutValue ) }); } From 7201564b105ebbeeef2a4607990a565d57b01d11 Mon Sep 17 00:00:00 2001 From: TheGreatMcPain Date: Thu, 21 Jan 2021 21:11:03 -0600 Subject: [PATCH 66/67] Update memoryjs --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 4c772c04..d724e9af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7268,7 +7268,7 @@ memory-fs@^0.5.0: "memoryjs@https://github.com/TheGreatMcPain/memoryjs.git": version "0.0.1" - resolved "https://github.com/TheGreatMcPain/memoryjs.git#b2f22662666e51116b2640dd8359c203d87402f0" + resolved "https://github.com/TheGreatMcPain/memoryjs.git#033b8750c7186f92f8c1bcec7c93be4e2fa71606" dependencies: bindings "^1.5.0" concentrate "^0.2.3" From 0b087a84852a752757811a9e5f1645b895b4e288 Mon Sep 17 00:00:00 2001 From: TheGreatMcPain Date: Tue, 9 Feb 2021 17:24:25 -0600 Subject: [PATCH 67/67] Update memoryjs and re-enable getProcesses code. I've implemented getProcesses in my fork of memoryjs. CrewLink can now be launched before launching Among Us, and should continue to work even if the game restarts. --- src/main/GameReader.ts | 8 ++++---- yarn.lock | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/GameReader.ts b/src/main/GameReader.ts index 89a25f5f..3638a6ab 100644 --- a/src/main/GameReader.ts +++ b/src/main/GameReader.ts @@ -1,6 +1,7 @@ import { DataType, findModule, + getProcesses, ModuleObject, openProcess, ProcessObject, @@ -57,10 +58,9 @@ export default class GameReader { gameCode = 'MENU'; checkProcessOpen(): void { - // const processOpen = getProcesses().find( - // (p) => p.szExeFile === 'Among Us.exe' - // ); - const processOpen = true; + const processOpen = getProcesses().find( + (p) => p.szExeFile === 'Among Us.exe' + ); if (!this.amongUs && processOpen) { try { this.amongUs = openProcess('Among Us.exe'); diff --git a/yarn.lock b/yarn.lock index d724e9af..bd593f33 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7268,7 +7268,7 @@ memory-fs@^0.5.0: "memoryjs@https://github.com/TheGreatMcPain/memoryjs.git": version "0.0.1" - resolved "https://github.com/TheGreatMcPain/memoryjs.git#033b8750c7186f92f8c1bcec7c93be4e2fa71606" + resolved "https://github.com/TheGreatMcPain/memoryjs.git#317193aab5d075f0a5ab4a5b5bea528eeab14c7b" dependencies: bindings "^1.5.0" concentrate "^0.2.3"