diff --git a/package.json b/package.json index 9d357bd..487b8cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "knockoutcitylauncher", - "version": "2.0.3", + "version": "2.1.1", "description": "Unofficial Knockout City Launcher", "main": "./out/main/index.js", "author": "IPGsystems", diff --git a/src/main/index.ts b/src/main/index.ts index 85931ce..a3c3bfe 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -13,8 +13,6 @@ import killProcess from 'tree-kill' import discordRPC from 'discord-rpc' import { is } from '@electron-toolkit/utils' import { IncomingMessage } from 'http' -import lodash from 'lodash' -import JSZip from 'jszip' remote.initialize() @@ -162,85 +160,73 @@ function createWindow(): void { }) ipcMain.on( - 'clean-gamedir-mods', - async (event, args: { basePath: string; gameVersion: number }) => { - event.returnValue = undefined - - const gameDirPath = path.join( - args.basePath, - args.gameVersion == 1 ? 'highRes' : 'lowRes', - 'KnockoutCity' - ) - - const outDirPath = path.join(gameDirPath, 'out') - fs.rmSync(outDirPath, { recursive: true, force: true }) - - const viperRootPath = path.join(gameDirPath, '.viper_root') - fs.rmSync(viperRootPath, { recursive: true, force: true }) - - const versionsPath = path.join(gameDirPath, 'version.json') - fs.rmSync(versionsPath, { recursive: true, force: true }) - - event.sender.send('cleaned-gamedir-mods') - } - ) - - ipcMain.on( - 'install-server-mods', + 'patch-game-client', async ( event, - args: { basePath: string; gameVersion: number; server: { name: string; addr: string } } + args: { basePath: string; gameVersion: number; serverType: 'public' | 'private' } ) => { event.returnValue = undefined - const serverModsDownloadPath = path.join(args.basePath, 'downloads', 'mods', args.server.name) - const serverModsVersionPath = path.join(serverModsDownloadPath, 'version.json') const gameDirPath = path.join(args.basePath, 'KnockoutCity') - const result = (await axios.get(`http://${args.server.addr}/mods/list`)).data + const gameExePath = path.join(gameDirPath, 'KnockoutCity.exe') + const backupGameExePath = path.join(gameDirPath, 'KnockoutCity.exe.bak') - const downloadMods = async (): Promise => { - if (result.length === 0) { - return - } + if (!fse.existsSync(backupGameExePath)) { + console.log(`Backing up ${gameExePath} to ${backupGameExePath}`) + await fse.copy(gameExePath, backupGameExePath) + } - const content = await ( - await fetch(`http://${args.server.addr}/mods/download`) - ).arrayBuffer() - - const zip = new JSZip() - await zip.loadAsync(content).then(async (contents) => { - for (const filename of Object.keys(contents.files)) { - await zip - .file(filename) - ?.async('nodebuffer') - .then((content) => { - const filePath = path.join(serverModsDownloadPath, filename) - fs.mkdirSync(path.dirname(filePath), { recursive: true }) - fs.writeFileSync(filePath, content) - }) - } - }) + let data = await fse.readFile(gameExePath).catch((error) => { + console.error('Failed to read Game File:', error) + throw Error('Failed to read Game File') + }) - fs.rmSync(serverModsDownloadPath, { recursive: true, force: true }) - fs.mkdirSync(serverModsDownloadPath, { recursive: true }) - fs.writeFileSync(serverModsVersionPath, JSON.stringify(result, null, 2)) - } + const patches: { + name: string + startAddress: number + endAddress: number + replacement: () => Buffer + }[] = [ + { + name: 'Signature Verification', + startAddress: 0x3cad481, + endAddress: 0x3cad485, + replacement: () => Buffer.from([0xb8, 0x01, 0x00, 0x00]) + }, + { + name: 'Auth Provider', + startAddress: 0x4f97230, + endAddress: 0x4f97233, + replacement: () => + Buffer.from(args.serverType === 'private' ? [0x64, 0x65, 0x76] : [0x78, 0x79, 0x7a]) + } + ] + + let needsPatching = false + for (const patch of patches) { + console.log(`Checking ${patch.name}...`) + const startBuffer = data.subarray(0, patch.startAddress) + const existingBytes = data.subarray(patch.startAddress, patch.endAddress) + const endBuffer = data.subarray(patch.endAddress) - fs.mkdirSync(serverModsDownloadPath, { recursive: true }) - if (fs.existsSync(serverModsVersionPath) && fs.statSync(serverModsVersionPath).isFile()) { - const versions = JSON.parse(fs.readFileSync(serverModsVersionPath).toString('utf-8')) + if (!existingBytes.equals(patch.replacement())) { + needsPatching = true - if (!lodash.isEqual(versions, result)) { - await downloadMods() + console.log(`Patching ${patch.name}...`) + data = Buffer.concat([startBuffer, patch.replacement(), endBuffer]) } - } else { - await downloadMods() } - fse.copySync(serverModsDownloadPath, gameDirPath) + if (needsPatching) { + console.log('Writing patched Game File...') + await fse.writeFile(gameExePath, data).catch((error) => { + console.error('Failed to write patched Game File:', error) + throw Error('Failed to write patched Game File') + }) + } - event.sender.send('installed-server-mods') + event.sender.send('patched-game-client') } ) diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 3842b47..1e0c742 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -23,8 +23,7 @@ declare global { getGameState: () => 'installed' | 'deprecated' | 'notInstalled' getGameInstalls: () => string[] - cleanGameDirMods: () => Promise - installServerMods: () => Promise + patchGameClient: () => Promise installGame: (props: { setGameState: (state: 'installed' | 'deprecated' | 'notInstalled' | 'installing') => void diff --git a/src/preload/index.ts b/src/preload/index.ts index 6dffd04..b8a975a 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -112,29 +112,14 @@ window.addEventListener('DOMContentLoaded', () => { } // @ts-ignore (define in dts) - window.cleanGameDirMods = (): Promise => { + window.patchGameClient = (): Promise => { return new Promise((resolve) => { - ipcRenderer.once('cleaned-gamedir-mods', () => resolve()) + ipcRenderer.once('patched-game-client', () => resolve()) - ipcRenderer.sendSync('clean-gamedir-mods', { - basePath: localStorage.getItem('gameDirectory'), - gameVersion: localStorage.getItem('gameVersion') - }) - }) - } - - // @ts-ignore (define in dts) - window.installServerMods = (): Promise => { - return new Promise((resolve) => { - ipcRenderer.once('installed-server-mods', () => resolve()) - - ipcRenderer.sendSync('install-server-mods', { + ipcRenderer.sendSync('patch-game-client', { basePath: localStorage.getItem('gameDirectory'), gameVersion: localStorage.getItem('gameVersion'), - server: { - name: localStorage.getItem('currServerName'), - addr: localStorage.getItem('currServer') - } + serverType: localStorage.getItem('currServerType') || 'private' }) }) } diff --git a/src/renderer/src/components/LaunchSection.tsx b/src/renderer/src/components/LaunchSection.tsx index 8d668bf..ff362e0 100644 --- a/src/renderer/src/components/LaunchSection.tsx +++ b/src/renderer/src/components/LaunchSection.tsx @@ -50,7 +50,8 @@ function LaunchSection(): JSX.Element { setCurrServerName(localStorage.getItem('currServerName') || 'localhost') setCurrServerType(localStorage.getItem('currServerType') || 'private') - await window.cleanGameDirMods() + setPopUpState('patchGameClient') + await window.patchGameClient() if (localStorage.getItem('currServerType') === 'public') { setPopUpState('authenticating') @@ -62,9 +63,6 @@ function LaunchSection(): JSX.Element { return alert('You must be logged in to use public servers!') } - setPopUpState('installing-server-mods') - await window.installServerMods() - setPopUpState('authenticating') const res = await axios @@ -89,7 +87,10 @@ function LaunchSection(): JSX.Element { setPopUpState(false) return window.launchGame({ setGameState, authkey }) - } else return window.launchGame({ setGameState }) + } else { + setPopUpState(false) + return window.launchGame({ setGameState }) + } }} /> ) diff --git a/src/renderer/src/components/popUp/index.tsx b/src/renderer/src/components/popUp/index.tsx index 69ceb32..581c184 100644 --- a/src/renderer/src/components/popUp/index.tsx +++ b/src/renderer/src/components/popUp/index.tsx @@ -7,11 +7,11 @@ import Authenticating from './views/Authenticating' import ConfirmLogout from './views/ConfirmLogout' import SelectUninstall from './views/SelectUninstall' import AccountSettings from './views/AccountSettings' +import PatchGameClient from './views/PatchGameClient' // States import { useUIState } from '@renderer/states/uiState' import { useAuthState } from '@renderer/states/authState' -import InstallingServerMods from './views/InstallingServerMods' function PopUp(): JSX.Element { const [popUpLoading, setPopUpLoading] = useState(false) @@ -272,8 +272,8 @@ function PopUp(): JSX.Element { return case 'accountSettings': return - case 'installing-server-mods': - return + case 'patchGameClient': + return default: return <> } diff --git a/src/renderer/src/components/popUp/views/InstallingServerMods.tsx b/src/renderer/src/components/popUp/views/PatchGameClient.tsx similarity index 74% rename from src/renderer/src/components/popUp/views/InstallingServerMods.tsx rename to src/renderer/src/components/popUp/views/PatchGameClient.tsx index 964cc70..353bbb2 100644 --- a/src/renderer/src/components/popUp/views/InstallingServerMods.tsx +++ b/src/renderer/src/components/popUp/views/PatchGameClient.tsx @@ -1,6 +1,6 @@ -import { Backdrop, Box, LinearProgress, Typography } from '@mui/material' +import { Backdrop, Box, Typography, LinearProgress } from '@mui/material' -export default function InstallingServerMods(): JSX.Element { +function PatchGameClient(): JSX.Element { return ( - Installing Server Mods + Patch Game Client -

Hang on while we are trying to install the required server mods

+

Patching the Game Client...

) } + +export default PatchGameClient