/* eslint-disable @typescript-eslint/no-explicit-any */
import { join } from 'path';
import { existsSync, mkdirSync } from 'fs';
import axios, { AxiosResponse } from 'axios';
import { spawn } from 'child_process';
import { Extract as unzip } from 'unzipper';
import { x as untar } from 'tar';
import config from '../../config.json';
/**
* SteamCmd exit code
* @type SteamCmdExitCode
* @enum
*/
export enum SteamCmdExitCode {
OK = 0,
INCORRECT_STEAM_GUARD_CODE = 5,
INCORRECT_PASSWORD = 7,
UNABLE_TO_INSTALL_APP = 8
}
/**
* Steam authentication credentials. To download the Slay the Spire game files from steam,
* an account that owns a purchased copy of the game is required
* @type SteamAuth
*/
export interface SteamAuth {
/**
* Steam account username
*/
readonly username: string;
/**
* Steam account password
*/
readonly password: string;
}
/**
* Optional configuration for {@link SteamCmd}
* @type SteamCmdConfig
*/
export interface SteamCmdConfig {
/**
* Directory to install SteamCmd files
*/
readonly installDir: string;
}
/**
* Contains platform specific information about SteamCMD
* @type Platform
* @private
*/
interface Platform {
host: string;
executable: string;
}
/**
* Manager of a SteamCmd instance. SteamCmd is used to download the Slay the Spire game files.
* To do so, a Steam account with a purchased copy of Slay the Spire is required. Documentation for
* SteamCmd can be found {@link https://developer.valvesoftware.com/wiki/SteamCMD|here}. This module
* only works on Windows
*/
export class SteamCmd {
private readonly platform: Platform;
private auth: SteamAuth | undefined | null;
private installDir: string;
/**
* Create an instance of {@link SteamCmd} with the credentials of a specified Steam account
* @param {SteamAuth} auth Steam account authentication for Steam account with a copy of
* Slay the Spire
* @param {SteamCmdConfig?} config Optional config for steam cmd
*
* @example
* const steamAuth: SteamAuth = { username: 'example_user', password: 'P@55W0RD' };
* const steamCmd = new SteamCmd(steamAuth);
*/
public constructor(auth?: SteamAuth | null, steamCmdConfig?: SteamCmdConfig) {
const platform = config.steamCmd.platform[process.platform];
if (platform === null || platform === undefined) {
throw Error('SteamCMD is not supported on this platform');
}
this.platform = platform;
this.auth = auth;
this.installDir = (steamCmdConfig && steamCmdConfig.installDir)
|| join(process.cwd(), '.steamcmd');
}
/**
* SteamCmd install directory
* @returns {string}
*/
public get installDirectory(): string {
return this.installDir;
}
/**
* Is SteamCmd installed
* @returns {boolean}
*/
public get isInstalled(): boolean {
return (
existsSync(this.installDir)
&& existsSync(join(this.installDir, this.platform.executable))
);
}
/**
* Download and extract SteamCmd files to the specified install directory. If no install
* directory was specified in the constructor with {@link SteamCmdConfig}, then SteamCmd is
* installed to the default install directory (./.steamcmd)
* @async
*
* @example
* // Install SteamCmd to './.steamcmd'
* const steamAuth: SteamAuth = { username: 'example_user', password: 'P@55W0RD' };
* const steamCmd = new SteamCmd(steamAuth);
* await steamCdm.install();
*/
public install(): Promise<void> {
return new Promise((resolve) => {
if (this.isInstalled) resolve();
axios.request({
method: 'get',
url: this.platform.host,
responseType: 'stream',
}).then((response: AxiosResponse) => {
mkdirSync(this.installDir, { recursive: true });
if (this.platform.executable === 'steamcmd.exe') {
response.data.pipe(unzip({ path: this.installDir }))
.on('finish', () => resolve());
}
if (this.platform.executable === 'steamcmd.sh') {
response.data.pipe(untar({ cwd: this.installDir }))
.on('finish', () => resolve());
}
});
});
}
/**
* Run a SteamCmd command
* @param {string} guardCode Steam guard code
* @param {Array<string>} command The SteamCmd command to run
* @async
* @return {Promise<SteamCmdExitCode>}
*
* @example
* // Install and validate the Counter Strike: Global Offensive dedicated server
* const steamAuth: SteamAuth = { username: 'example_user', password: 'P@55W0RD' };
* const steamCmd = new SteamCmd(steamAuth);
* await steamCmd.runCommand(['+force_install_directory', './cs_go/', '+app_update', '740',
* 'validate', '+quit'], '12345');
*/
public async runCommand(
command: Array<string>,
guardCode?: string,
): Promise<SteamCmdExitCode> {
// Returns a promise directly because TS does not like using the async/await syntax
// when resolving with `process.on('exit')`
return new Promise<SteamCmdExitCode>((resolve, reject) => {
if (!this.isInstalled) {
reject(
Error('SteamCmd not installed in the specified installdir. Try running SteamCmd.install()?'),
);
}
let authSubCommand: Array<string> = [];
if (this.auth) {
authSubCommand = [
'+login',
this.auth.username,
this.auth.password,
guardCode || '',
];
}
const childProcess = spawn(
join(this.installDir, this.platform.executable),
[...authSubCommand, ...command, '+quit'],
);
childProcess.on('exit', (code: SteamCmdExitCode) => {
resolve(code);
});
});
}
/**
* Install a Steam game with the specified app ID
* @param {string} guardCode Steam guard code
* @param {number} appId ID of the steam app to install
* @param {string} gameInstallDir Directory to install the game to
* @async
*
* @example
* // Install Slay the Spire to './.sts'
* const steamAuth: SteamAuth = { username: 'example_user', password: 'P@55W0RD' };
* const steamCmd = new SteamCmd(steamAuth);
* await steamCmd.installApp(646570, './.sts', '12345');
*/
async installApp(
appId: number,
gameInstallDir: string,
guardCode?: string,
): Promise<any> {
const exitCode: SteamCmdExitCode = await this.runCommand(
['+force_install_dir', gameInstallDir, '+app_update', appId.toString()],
guardCode,
);
return exitCode;
}
/**
* Get the description of a specified SteamCmd {@link SteamCmdExitCode}
* @param {SteamCmdExitCode} code The exit code to get the description of
* @returns {string}
*
* @example
* const exitDescription: string = getExitCodeDescription(
* SteamCmdExitCode.UNABLE_TO_INSTALL_APP);
* console.log(exitDescription); // 'unable to install app'
*/
public static getExitCodeDescription(code: SteamCmdExitCode): string {
return Object({
0: 'OK',
5: 'incorrect steam guard code',
7: 'incorrect password',
8: 'unable to install app',
})[code];
}
/**
* Check if a steam app is installed
* @param {string} gameInstallDir Install directory of steam app
* @returns {boolean}
* @static
*
* @example
* const isSlayTheSpireInstalled: boolean = SteamCmd.isAppInstalled('./.sts'); // false
*/
public static isAppInstalled(gameInstallDir: string): boolean {
return existsSync(gameInstallDir);
}
}
Source