From 3d705d51477212b8d96d21ca7d3d290caf760f87 Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Sun, 1 Oct 2023 16:09:54 -0700 Subject: [PATCH 01/15] Add working launch configs --- src/papyrus-lang-vscode/package.json | 77 ++++++--- .../src/PapyrusExtension.ts | 4 +- .../src/debugger/DebugLauncherService.ts | 163 ++++++++++++++++++ .../PapyrusDebugAdapterDescriptorFactory.ts | 71 +++++++- .../debugger/PapyrusDebugAdapterTracker.ts | 19 +- .../PapyrusDebugConfigurationProvider.ts | 155 +++++++++++++++-- .../src/debugger/PapyrusDebugSession.ts | 99 ++++++++++- 7 files changed, 533 insertions(+), 55 deletions(-) create mode 100644 src/papyrus-lang-vscode/src/debugger/DebugLauncherService.ts diff --git a/src/papyrus-lang-vscode/package.json b/src/papyrus-lang-vscode/package.json index 932caf4e..feadf977 100644 --- a/src/papyrus-lang-vscode/package.json +++ b/src/papyrus-lang-vscode/package.json @@ -62,50 +62,83 @@ }, "projectPath": { "type": "string" + }, + "launchType": { + "type": "string", + "enum": [ + "XSE", + "mo2" + ] + }, + "XSELoaderPath": { + "type": "string" + }, + "mo2Config": { + "type": "object", + "properties": { + "mo2Path": { + "type": "string" + }, + "shortcut": { + "type": "string" + }, + "modsFolder": { + "type": "string" + }, + "args": { + "type": "array" + } + }, + "required": [ + "mo2Path", + "shortcut", + "modsFolder" + ] } }, "required": [ - "game" + "game", + "launchType" ] } }, "configurationSnippets": [ { - "label": "Papyrus: Fallout 4", + "label": "Papyrus: Attach", "body": { - "name": "Fallout 4", + "name": "Attach (${2:Fallout 4})", "type": "papyrus", + "game": "${3:fallout4}", "request": "attach", - "game": "fallout4" - } - }, - { - "label": "Papyrus: Fallout 4 (with .ppj)", - "body": { - "name": "Fallout 4 Project", - "type": "papyrus", - "request": "attach", - "game": "fallout4", "projectPath": "^\"\\${workspaceFolder}/${1:Project.ppj}\"" } }, { - "label": "Papyrus: Skyrim Special Edition/Anniversary Edition", + "label": "Papyrus: Launch SKSE/F4SE Loader)", "body": { - "name": "Skyrim", + "name": "Launch with ${4:F4SE}_loader (${2:Fallout 4})", "type": "papyrus", - "request": "attach", - "game": "skyrimSpecialEdition" + "game": "${3:fallout4}", + "request": "launch", + "projectPath": "^\"\\${workspaceFolder}/${1:Project.ppj}\"", + "launchType": "skse", + "XSELoaderPath": "^\"${5:C:/Program Files (x86)/Steam/steamapps/common/Fallout 4/F4SE/F4SE_loader.exe}\"" } }, { - "label": "Papyrus: Skyrim Special Edition/Anniversary Edition (with .ppj)", + "label": "Papyrus: Launch (ModOrganizer2 Loader)", "body": { - "name": "Skyrim Special Edition/Anniversary Edition Project", + "name": "Launch with ModOrganizer2 (${2:Fallout 4})", "type": "papyrus", - "request": "attach", - "game": "skyrimSpecialEdition", - "projectPath": "^\"\\${workspaceFolder}/${1:Project.ppj}\"" + "game": "${3:fallout4}", + "request": "launch", + "launchType": "mo2", + "projectPath": "^\"\\${workspaceFolder}/${1:Project.ppj}\"", + "mo2Config": { + "mo2Path": "^\"${6:C:/Modding/MO2/ModOrganizer.exe}\"", + "shortcut": "^\"${7:moshortcut://Skyrim Special Edition:SKSE}\"", + "modsFolder": "^\"${8:\\${env:LOCALAPPDATA\\}/ModOrganizer/Fallout 4/mods}\"" + } } } ] diff --git a/src/papyrus-lang-vscode/src/PapyrusExtension.ts b/src/papyrus-lang-vscode/src/PapyrusExtension.ts index 82eff719..4639b0e1 100644 --- a/src/papyrus-lang-vscode/src/PapyrusExtension.ts +++ b/src/papyrus-lang-vscode/src/PapyrusExtension.ts @@ -26,6 +26,7 @@ import { GenerateProjectCommand } from './features/commands/GenerateProjectComma import { showWelcome } from './features/WelcomeHandler'; import { ShowWelcomeCommand } from './features/commands/ShowWelcomeCommand'; import { Container } from 'inversify'; +import { IDebugLauncherService, DebugLauncherService } from "./debugger/DebugLauncherService"; class PapyrusExtension implements Disposable { private readonly _serviceContainer: Container; @@ -66,6 +67,7 @@ class PapyrusExtension implements Disposable { this._serviceContainer.bind(ICreationKitInfoProvider).to(CreationKitInfoProvider); this._serviceContainer.bind(ILanguageClientManager).to(LanguageClientManager); this._serviceContainer.bind(IDebugSupportInstallService).to(DebugSupportInstallService); + this._serviceContainer.bind(IDebugLauncherService).to(DebugLauncherService); this._configProvider = this._serviceContainer.get(IExtensionConfigProvider); this._clientManager = this._serviceContainer.get(ILanguageClientManager); @@ -78,7 +80,7 @@ class PapyrusExtension implements Disposable { this._debugAdapterDescriptorFactory = this._serviceContainer.resolve(PapyrusDebugAdapterDescriptorFactory); this._installDebugSupportCommand = this._serviceContainer.resolve(InstallDebugSupportCommand); - this._debugAdapterTrackerFactory = new PapyrusDebugAdapterTrackerFactory(); + this._debugAdapterTrackerFactory = this._serviceContainer.resolve(PapyrusDebugAdapterTrackerFactory); this._attachCommand = new AttachDebuggerCommand(); diff --git a/src/papyrus-lang-vscode/src/debugger/DebugLauncherService.ts b/src/papyrus-lang-vscode/src/debugger/DebugLauncherService.ts new file mode 100644 index 00000000..121587f6 --- /dev/null +++ b/src/papyrus-lang-vscode/src/debugger/DebugLauncherService.ts @@ -0,0 +1,163 @@ +import { inject, injectable, interfaces } from 'inversify'; +import { IExtensionConfigProvider } from '../ExtensionConfigProvider'; +import { CancellationToken, CancellationTokenSource, window } from 'vscode'; +import { take } from 'rxjs/operators'; +import { IPathResolver } from '../common/PathResolver'; +import { getDisplayNameForGame } from '../PapyrusGame'; +import { PapyrusGame } from "../PapyrusGame"; +import { ILanguageClientManager } from '../server/LanguageClientManager'; +import { ClientHostStatus } from '../server/LanguageClientHost'; +import { getGameIsRunning, getGamePIDs, mkdirIfNeeded } from '../Utilities'; + +import * as path from 'path'; +import * as fs from 'fs'; +import { promisify } from 'util'; + +import md5File from 'md5-file'; +import { ChildProcess, spawn } from 'node:child_process'; +import { timer } from 'rxjs'; + +const exists = promisify(fs.exists); +const copyFile = promisify(fs.copyFile); +const removeFile = promisify(fs.unlink); + +export enum DebugLaunchState { + success, + launcherError, + gameFailedToStart, + multipleGamesRunning, + cancelled, +} +export interface IDebugLauncherService { + tearDownAfterDebug(): Promise; + runLauncher( + launcherPath: string, + launcherArgs: string[], + game: PapyrusGame, + cancellationToken?: CancellationToken + ): Promise; +} + +@injectable() +export class DebugLauncherService implements IDebugLauncherService { + private readonly _configProvider: IExtensionConfigProvider; + private readonly _languageClientManager: ILanguageClientManager; + private readonly _pathResolver: IPathResolver; + + // TODO: Move this stuff into the global Context + private cancellationTokenSource: CancellationTokenSource | undefined; + private launcherProcess: ChildProcess | undefined; + private gamePID: number | undefined; + private currentGame: PapyrusGame | undefined; + constructor( + @inject(ILanguageClientManager) languageClientManager: ILanguageClientManager, + @inject(IExtensionConfigProvider) configProvider: IExtensionConfigProvider, + @inject(IPathResolver) pathResolver: IPathResolver + ) { + this._languageClientManager = languageClientManager; + this._configProvider = configProvider; + this._pathResolver = pathResolver; + } + + async tearDownAfterDebug() { + if (this.launcherProcess) { + this.launcherProcess.removeAllListeners(); + this.launcherProcess.kill(); + } + if (this.gamePID && this.currentGame && (await getGameIsRunning(this.currentGame))) { + process.kill(this.gamePID); + } + this.launcherProcess = undefined; + this.gamePID = undefined; + this.currentGame = undefined; + return true; + } + + async keepSleepingUntil(startTime: number, timeout: number) { + const currentTime = new Date().getTime(); + + if (currentTime > startTime + timeout) { + return false; + } + await new Promise((resolve) => setTimeout(resolve, 200)); + return true; + } + + async cancelLaunch() { + if (this.cancellationTokenSource) { + this.cancellationTokenSource.cancel(); + } + } + + async runLauncher( + launcherPath: string, + launcherArgs: string[], + game: PapyrusGame, + cancellationToken: CancellationToken | undefined + ): Promise { + await this.tearDownAfterDebug(); + if (!cancellationToken) { + this.cancellationTokenSource = new CancellationTokenSource(); + cancellationToken = this.cancellationTokenSource.token; + } + this.currentGame = game; + this.launcherProcess = spawn(launcherPath, launcherArgs, { + detached: true, + stdio: 'ignore', + }); + + // get the current system time + const GameStartTimeout = 10000; + let startTime = new Date().getTime(); + // wait for the games process to start + while (!cancellationToken.isCancellationRequested) { + if (!(await getGameIsRunning(game)) && (await this.keepSleepingUntil(startTime, GameStartTimeout))) { + // check if the launcher process failed to launch, or exited and returned an error + if ( + !this.launcherProcess || + (this.launcherProcess.exitCode !== null && this.launcherProcess.exitCode !== 0) + ) { + return DebugLaunchState.launcherError; + } + } else { + break; + } + } + + if (cancellationToken.isCancellationRequested) { + await this.tearDownAfterDebug(); + return DebugLaunchState.cancelled; + } + // we can't get the PID of the game from the launcher process because + // both MO2 and the script extender loaders forks and deatches the game process + let gamePIDs = await getGamePIDs(game); + + if (gamePIDs.length === 0) { + return DebugLaunchState.gameFailedToStart; + } + + if (gamePIDs.length > 1) { + return DebugLaunchState.multipleGamesRunning; + } + this.gamePID = gamePIDs[0]; + + startTime = new Date().getTime(); + + // wait for the game to fully load + while (!cancellationToken.isCancellationRequested) { + if (!(await this.keepSleepingUntil(startTime, GameStartTimeout))) { + break; + } + } + + if (cancellationToken.isCancellationRequested) { + await this.tearDownAfterDebug(); + return DebugLaunchState.cancelled; + } + + return DebugLaunchState.success; + } +} + +export const IDebugLauncherService: interfaces.ServiceIdentifier = + Symbol('DebugLauncherService'); diff --git a/src/papyrus-lang-vscode/src/debugger/PapyrusDebugAdapterDescriptorFactory.ts b/src/papyrus-lang-vscode/src/debugger/PapyrusDebugAdapterDescriptorFactory.ts index 09b171af..07357c88 100644 --- a/src/papyrus-lang-vscode/src/debugger/PapyrusDebugAdapterDescriptorFactory.ts +++ b/src/papyrus-lang-vscode/src/debugger/PapyrusDebugAdapterDescriptorFactory.ts @@ -8,6 +8,9 @@ import { commands, Uri, env, + CancellationTokenSource, + MessageOptions, + MessageItem } from 'vscode'; import { PapyrusGame, @@ -26,6 +29,10 @@ import { IDebugSupportInstallService, DebugSupportInstallState } from './DebugSu import { ILanguageClientManager } from '../server/LanguageClientManager'; import { showGameDisabledMessage, showGameMissingMessage } from '../features/commands/InstallDebugSupportCommand'; import { inject, injectable } from 'inversify'; +import { ChildProcess, spawn } from 'child_process'; +import { DebugLaunchState, IDebugLauncherService } from './DebugLauncherService'; +import { CancellationToken } from 'vscode-languageclient'; +import { openSync, readSync, readFileSync } from 'fs'; const noopExecutable = new DebugAdapterExecutable('node', ['-e', '""']); @@ -43,6 +50,8 @@ function getDefaultPortForGame(game: PapyrusGame) { return game === PapyrusGame.fallout4 ? 2077 : 43201; } + + @injectable() export class PapyrusDebugAdapterDescriptorFactory implements DebugAdapterDescriptorFactory { private readonly _languageClientManager: ILanguageClientManager; @@ -50,6 +59,7 @@ export class PapyrusDebugAdapterDescriptorFactory implements DebugAdapterDescrip private readonly _configProvider: IExtensionConfigProvider; private readonly _pathResolver: IPathResolver; private readonly _debugSupportInstaller: IDebugSupportInstallService; + private readonly _debugLauncher: IDebugLauncherService; private readonly _registration: Disposable; constructor( @@ -57,18 +67,19 @@ export class PapyrusDebugAdapterDescriptorFactory implements DebugAdapterDescrip @inject(ICreationKitInfoProvider) creationKitInfoProvider: ICreationKitInfoProvider, @inject(IExtensionConfigProvider) configProvider: IExtensionConfigProvider, @inject(IPathResolver) pathResolver: IPathResolver, - @inject(IDebugSupportInstallService) debugSupportInstaller: IDebugSupportInstallService + @inject(IDebugSupportInstallService) debugSupportInstaller: IDebugSupportInstallService, + @inject(IDebugLauncherService) debugLauncher: IDebugLauncherService ) { this._languageClientManager = languageClientManager; this._creationKitInfoProvider = creationKitInfoProvider; this._configProvider = configProvider; this._pathResolver = pathResolver; this._debugSupportInstaller = debugSupportInstaller; - + this._debugLauncher = debugLauncher; this._registration = debug.registerDebugAdapterDescriptorFactory('papyrus', this); } - private async ensureReadyFlow(game: PapyrusGame) { + private async ensureGameInstalled(game: PapyrusGame) { const installState = await this._debugSupportInstaller.getInstallState(game); switch (installState) { @@ -133,7 +144,10 @@ export class PapyrusDebugAdapterDescriptorFactory implements DebugAdapterDescrip showGameMissingMessage(game); return false; } + return true; + } + async ensureGameRunning(game: PapyrusGame) { if (!(await getGameIsRunning(game))) { const selectedGameRunningOption = await window.showWarningMessage( `Make sure that ${getDisplayNameForGame(game)} is running and is either in-game or at the main menu.`, @@ -158,13 +172,58 @@ export class PapyrusDebugAdapterDescriptorFactory implements DebugAdapterDescrip if (game !== PapyrusGame.fallout4 && game !== PapyrusGame.skyrimSpecialEdition) { throw new Error(`'${game}' is not supported by the Papyrus debugger.`); } - - if (!(await this.ensureReadyFlow(game))) { + if (!await this.ensureGameInstalled(game)){ session.configuration.noop = true; - return noopExecutable; } + let launched = DebugLaunchState.success; + if (session.configuration.request === 'launch'){ + if (await getGameIsRunning(game)){ + throw new Error(`'${getDisplayNameForGame(game)}' is already running. Please close it before launching the debugger.`); + } + // run the launcher with the args from the configuration + // if the launcher is MO2 + let launcherPath: string, launcherArgs: string[] + if(session.configuration.launchType === 'mo2') { + launcherPath = session.configuration.mo2Config?.MO2EXEPath || ""; + const shortcut = session.configuration.mo2Config?.shortcut; + const args = session.configuration.mo2Config?.args || []; + launcherArgs = shortcut ? [shortcut] : []; + if (args) { + launcherArgs = launcherArgs.concat(args); + } + } else if(session.configuration.launchType === 'XSE') { + launcherPath = session.configuration.XSELoaderPath || ""; + launcherArgs = session.configuration.args || []; + } else { + // throw an error indicated the launch configuration is invalid + throw new Error(`'Invalid launch configuration.`); + } + + const cancellationSource = new CancellationTokenSource(); + const cancellationToken = cancellationSource.token; + + let wait_message = window.setStatusBarMessage(`Waiting for ${getDisplayNameForGame(game)} to start...`, 30000); + launched = await this._debugLauncher.runLauncher( launcherPath, launcherArgs, game, cancellationToken) + wait_message.dispose(); + } + if (launched != DebugLaunchState.success){ + if (launched === DebugLaunchState.cancelled){ + session.configuration.noop = true; + return noopExecutable; + } + if (launched === DebugLaunchState.multipleGamesRunning){ + const errMessage = `Multiple ${getDisplayNameForGame(game)} instances are running, shut them down and try again.`; + window.showErrorMessage(errMessage); + } + // throw an error indicating the launch failed + throw new Error(`'${game}' failed to launch.`); + // attach + } else if (!await this.ensureGameRunning(game)) { + session.configuration.noop = true; + return noopExecutable; + } const config = (await this._configProvider.config.pipe(take(1)).toPromise())[game]; const creationKitInfo = await this._creationKitInfoProvider.infos.get(game)!.pipe(take(1)).toPromise(); diff --git a/src/papyrus-lang-vscode/src/debugger/PapyrusDebugAdapterTracker.ts b/src/papyrus-lang-vscode/src/debugger/PapyrusDebugAdapterTracker.ts index db094a68..d1aa75c7 100644 --- a/src/papyrus-lang-vscode/src/debugger/PapyrusDebugAdapterTracker.ts +++ b/src/papyrus-lang-vscode/src/debugger/PapyrusDebugAdapterTracker.ts @@ -1,27 +1,37 @@ +import { inject, injectable } from 'inversify'; import { DebugAdapterTrackerFactory, DebugSession, DebugAdapterTracker, window, Disposable, debug } from 'vscode'; +import { IDebugLauncherService } from './DebugLauncherService'; +@injectable() export class PapyrusDebugAdapterTrackerFactory implements DebugAdapterTrackerFactory, Disposable { + private readonly _debugLauncher: IDebugLauncherService; private readonly _registration: Disposable; - constructor() { + constructor( + @inject(IDebugLauncherService) debugLauncher: IDebugLauncherService + ) { + this._debugLauncher = debugLauncher; this._registration = debug.registerDebugAdapterTrackerFactory('papyrus', this); } async createDebugAdapterTracker(session: DebugSession): Promise { - return new PapyrusDebugAdapterTracker(session); + return new PapyrusDebugAdapterTracker(session, this._debugLauncher); } dispose() { this._registration.dispose(); } } - export class PapyrusDebugAdapterTracker implements DebugAdapterTracker { + private readonly _debugLauncher: IDebugLauncherService; private readonly _session: DebugSession; private _showErrorMessages = true; - constructor(session: DebugSession) { + constructor(session: DebugSession, + debugLauncher: IDebugLauncherService + ) { + this._debugLauncher = debugLauncher; this._session = session; } @@ -38,6 +48,7 @@ export class PapyrusDebugAdapterTracker implements DebugAdapterTracker { } onExit(code: number | undefined, signal: string | undefined) { + this._debugLauncher.tearDownAfterDebug(); if (!this._showErrorMessages || this._session.configuration.noop) { return; } diff --git a/src/papyrus-lang-vscode/src/debugger/PapyrusDebugConfigurationProvider.ts b/src/papyrus-lang-vscode/src/debugger/PapyrusDebugConfigurationProvider.ts index 8c1d0d6d..bb46f511 100644 --- a/src/papyrus-lang-vscode/src/debugger/PapyrusDebugConfigurationProvider.ts +++ b/src/papyrus-lang-vscode/src/debugger/PapyrusDebugConfigurationProvider.ts @@ -1,7 +1,8 @@ -import { injectable } from 'inversify'; -import { DebugConfigurationProvider, CancellationToken, WorkspaceFolder, debug, Disposable } from 'vscode'; -import { PapyrusGame } from '../PapyrusGame'; -import { IPapyrusDebugConfiguration } from './PapyrusDebugSession'; +import { inject, injectable } from 'inversify'; +import { ProviderResult, DebugConfigurationProvider, CancellationToken, WorkspaceFolder, debug, Disposable, DebugConfiguration } from 'vscode'; +import { IPathResolver } from '../common/PathResolver'; +import { PapyrusGame } from "../PapyrusGame"; +import { MO2Config, IPapyrusDebugConfiguration } from './PapyrusDebugSession'; // TODO: Auto install F4SE plugin // TODO: Warn if port is not open/if Fallout4.exe is not running @@ -10,11 +11,16 @@ import { IPapyrusDebugConfiguration } from './PapyrusDebugSession'; // TODO: Resolve project from whichever that includes the active editor file. // TODO: Provide configurations based on .ppj files in current directory. + @injectable() export class PapyrusDebugConfigurationProvider implements DebugConfigurationProvider, Disposable { private readonly _registration: Disposable; + private readonly _pathResolver: IPathResolver; - constructor() { + constructor( + @inject(IPathResolver) pathResolver: IPathResolver + ) { + this._pathResolver = pathResolver; this._registration = debug.registerDebugConfigurationProvider('papyrus', this); } @@ -22,24 +28,137 @@ export class PapyrusDebugConfigurationProvider implements DebugConfigurationProv _folder: WorkspaceFolder | undefined, _token?: CancellationToken ): Promise { + let PapyrusAttach = { + type: 'papyrus', + name: 'Fallout 4', + game: PapyrusGame.fallout4, + request: 'attach', + projectPath: '${workspaceFolder}/${1:Project.ppj}', + } as IPapyrusDebugConfiguration; + let PapyrusMO2Launch = { + type: 'papyrus', + name: 'Fallout 4 (Launch with MO2)', + game: PapyrusGame.fallout4, + request: 'launch', + launchType: 'mo2', + mo2Config: { + MO2EXEPath: 'C:/Modding/MO2/ModOrganizer.exe', + shortcut: 'moshortcut://Fallout 4:F4SE', + modsFolder: '${env:LOCALAPPDATA}/ModOrganizer/Fallout 4/mods', + args: ['-skipIntro'] + } as MO2Config + } as IPapyrusDebugConfiguration; + let PapyruseXSELaunch = { + type: 'papyrus', + name: 'Fallout 4 (Launch with F4SE)', + game: PapyrusGame.fallout4, + request: 'launch', + launchType: 'XSE', + XSELoaderPath: 'C:/Program Files (x86)/Steam/steamapps/common/Fallout 4/f4se_loader.exe', + args: ['-skipIntro'] + } as IPapyrusDebugConfiguration; return [ - { - type: 'papyrus', - name: 'Fallout 4', - request: 'attach', - game: PapyrusGame.fallout4, - }, + PapyrusAttach, + PapyrusMO2Launch, + PapyruseXSELaunch ]; } - // async resolveDebugConfiguration( - // folder: WorkspaceFolder | undefined, - // debugConfiguration: DebugConfiguration, - // token?: CancellationToken - // ): Promise { - // return null; - // } + async resolveDebugConfiguration( + folder: WorkspaceFolder | undefined, + debugConfiguration: IPapyrusDebugConfiguration, + token?: CancellationToken + ): Promise { + if (debugConfiguration.game !== undefined && debugConfiguration.request !== undefined) + { + if (debugConfiguration.request === 'launch') + { + if (debugConfiguration.launchType === 'mo2') + { + if (debugConfiguration.mo2Config !== undefined && debugConfiguration.mo2Config.modsFolder !== undefined && debugConfiguration.mo2Config.MO2EXEPath !== undefined) + { + return debugConfiguration; + } + } + else if (debugConfiguration.launchType === 'XSE') + { + if (debugConfiguration.XSELoaderPath !== undefined) + { + return debugConfiguration; + } + } + } + else if (debugConfiguration.request === 'attach') + { + return debugConfiguration; + } + } + throw new Error("Invalid debug configuration."); + return undefined; + } + + async substituteEnvVars(string: string): Promise { + let appdata = process.env.LOCALAPPDATA; + let username = process.env.USERNAME; + if (appdata){ + string = string.replace('${env:LOCALAPPDATA}', appdata); + } + if (username){ + string = string.replace('${env:USERNAME}', username); + } + return string; + } + + // TODO: Check that all of these exist + async prepMo2Config(mo2Config: MO2Config, game: PapyrusGame): Promise { + let modFolder = mo2Config.modsFolder || await this._pathResolver.getModDirectoryPath(game); + return { + MO2EXEPath: await this.substituteEnvVars(mo2Config.MO2EXEPath), + shortcut: mo2Config.shortcut, + modsFolder: await this.substituteEnvVars(mo2Config.modsFolder || ""), + profile: mo2Config.profile || "Default", + profilesFolder: mo2Config.profilesFolder ? await this.substituteEnvVars(mo2Config?.profilesFolder) : undefined, + args: mo2Config.args || [] + } as MO2Config; + } + + async resolveDebugConfigurationWithSubstitutedVariables( + folder: WorkspaceFolder | undefined, + debugConfiguration: IPapyrusDebugConfiguration, + token?: CancellationToken + ): Promise { + if (debugConfiguration.request === 'launch') + { + if (debugConfiguration.launchType === 'mo2') + { + if (debugConfiguration.mo2Config === undefined) + { + return undefined; + } + debugConfiguration.mo2Config = await this.prepMo2Config(debugConfiguration.mo2Config, debugConfiguration.game); + return debugConfiguration + } + + else if (debugConfiguration.launchType === 'XSE') + { + if(debugConfiguration.XSELoaderPath === undefined) + { + return undefined; + } + debugConfiguration.XSELoaderPath = await this.substituteEnvVars(debugConfiguration.XSELoaderPath); + return debugConfiguration; + } + } + // else... + else if (debugConfiguration.request === 'attach') + { + return debugConfiguration; + } + throw new Error("Invalid debug configuration."); + return undefined; + } + dispose() { this._registration.dispose(); } diff --git a/src/papyrus-lang-vscode/src/debugger/PapyrusDebugSession.ts b/src/papyrus-lang-vscode/src/debugger/PapyrusDebugSession.ts index 864c2e96..3f994db5 100644 --- a/src/papyrus-lang-vscode/src/debugger/PapyrusDebugSession.ts +++ b/src/papyrus-lang-vscode/src/debugger/PapyrusDebugSession.ts @@ -1,13 +1,104 @@ import { DebugSession, DebugConfiguration } from 'vscode'; -import { PapyrusGame } from '../PapyrusGame'; +import { PapyrusGame } from "../PapyrusGame"; export interface IPapyrusDebugSession extends DebugSession { readonly type: 'papyrus'; readonly configuration: IPapyrusDebugConfiguration; } +export interface MO2Config { + /** + * The path to the Mod Organizer 2 executable + * + * Example: + * - "C:/Program Files/Mod Organizer 2/ModOrganizer.exe" + */ + MO2EXEPath: string; + /** + * The shortcut URI for the Mod Organizer 2 profile to launch + * + * You can get this from the Mod Organizer 2 shortcut menu + * + * Example: + * - "moshortcut://Skyrim Special Edition:SKSE" + */ + shortcut: string; + /** + * The path to the Mod Organizer 2 mods folder + * If not specified, defaults to the globally configured mods folder. + * + * Example: + * - "C:/Users/${USERNAME}/AppData/Local/ModOrganizer/Fallout 4/mods" + */ + modsFolder?: string; + /** + * The name of the Mod Organizer 2 profile to launch with + * Defaults to "Default" + */ + profile?: string; + /** + * The path to the "profiles" folder for the Mod Organizer 2 instance. + * + * If you have specified a custom mods folder in your MO2 instance configuration, + * you must specify the profiles folder here. + * + * If not specified, defaults to the "profiles" folder in the same parent directory as the mods folder. + * + * Example: + * - "C:/Users/${USERNAME}/AppData/Local/ModOrganizer/Fallout 4/profiles" + */ + profilesFolder?: string; + + /** + * Additional arguments to pass to Mod Organizer 2 + */ + args?: string[]; +} + export interface IPapyrusDebugConfiguration extends DebugConfiguration { - readonly game: PapyrusGame; - readonly projectPath?: string; - readonly port?: number; + /** + * The game to debug ('fallout4', 'skyrim', 'skyrimSpecialEdition') + */ + game: PapyrusGame; + /** + * The path to the project to debug + */ + projectPath?: string; + port?: number; + /** + * The type of debug request + * - 'attach': Attaches to a running game + * - 'launch': Launches the game + */ + request: 'attach' | 'launch'; + /** + * The type of launch to use + * + * - 'XSE': Launches the game using SKSE/F4SE without a mod manager + * - 'mo2': Launches the game using Mod Organizer 2 + * */ + launchType?: 'XSE' | 'mo2'; + /** + * + * Configuration for Mod Organizer 2 + * + * Only used if launchType is 'mo2' + * + */ + mo2Config?: MO2Config; + + /** + * The path to the f4se/skse loader executable + * + * Examples: + * - "C:/Program Files (x86)/Steam/steamapps/common/Skyrim Special Edition/skse64_loader.exe" + * - "C:/Program Files (x86)/Steam/steamapps/common/Fallout 4/f4se_loader.exe" + */ + XSELoaderPath?: string; + + /** + * Additional arguments to pass + * */ + args?: string[]; + } From fe7bf6f1d0c16717ec5941639a3b26276feb61b6 Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Sun, 1 Oct 2023 16:10:51 -0700 Subject: [PATCH 02/15] add WIP MO2 configurator --- .../src/common/PathResolver.ts | 14 +- .../src/common/constants.ts | 7 +- .../debugger/DebugSupportInstallService.ts | 16 +- .../src/debugger/MO2Helpers.ts | 331 ++++++++++++ .../debugger/MO2LaunchDescriptorFactory.ts | 482 ++++++++++++++++++ 5 files changed, 837 insertions(+), 13 deletions(-) create mode 100644 src/papyrus-lang-vscode/src/debugger/MO2Helpers.ts create mode 100644 src/papyrus-lang-vscode/src/debugger/MO2LaunchDescriptorFactory.ts diff --git a/src/papyrus-lang-vscode/src/common/PathResolver.ts b/src/papyrus-lang-vscode/src/common/PathResolver.ts index b3f7031e..5d5a75eb 100644 --- a/src/papyrus-lang-vscode/src/common/PathResolver.ts +++ b/src/papyrus-lang-vscode/src/common/PathResolver.ts @@ -11,7 +11,8 @@ import { IExtensionContext } from '../common/vscode/IocDecorators'; import { PapyrusGame, getScriptExtenderName } from '../PapyrusGame'; import { inDevelopmentEnvironment } from '../Utilities'; -import { IExtensionConfigProvider, IGameConfig } from '../ExtensionConfigProvider'; +import { IExtensionConfigProvider, IGameConfig, IExtensionConfig } from '../ExtensionConfigProvider'; +import { PDSModName } from './constants'; const exists = promisify(fs.exists); @@ -59,8 +60,9 @@ export class PathResolver implements IPathResolver { return `Data/${getScriptExtenderName(game)}/Plugins`; } + // TODO: Refactor this properly // For mod managers. The whole directory for the mod is "Data" so omit that part. - private async _getModMgrExtenderPluginPath(game: PapyrusGame) { + public static _getModMgrExtenderPluginRelativePath(game: PapyrusGame) { return `${getScriptExtenderName(game)}/Plugins`; } // Public Methods @@ -114,14 +116,14 @@ export class PathResolver implements IPathResolver { return resolveInstallPath(game, config.installPath, this._context); } + // TODO: Refactor this properly. public async getDebugPluginInstallPath(game: PapyrusGame, legacy?: boolean): Promise { const modDirectoryPath = await this.getModDirectoryPath(game); if (modDirectoryPath) { return path.join( - modDirectoryPath, - 'Papyrus Debug Extension', - await this._getModMgrExtenderPluginPath(game), + modDirectoryPath, PDSModName, + PathResolver._getModMgrExtenderPluginRelativePath(game), getPluginDllName(game, legacy) ); } else { @@ -179,7 +181,7 @@ function getToolGameName(game: PapyrusGame): string { /*** External paths (ones that are not "ours") */ /************************************************************************* */ -function getRegistryKeyForGame(game: PapyrusGame) { +export function getRegistryKeyForGame(game: PapyrusGame) { switch (game) { case PapyrusGame.fallout4: return 'Fallout4'; diff --git a/src/papyrus-lang-vscode/src/common/constants.ts b/src/papyrus-lang-vscode/src/common/constants.ts index 2c70b2d8..3c010cdc 100644 --- a/src/papyrus-lang-vscode/src/common/constants.ts +++ b/src/papyrus-lang-vscode/src/common/constants.ts @@ -5,5 +5,10 @@ export const extensionId = 'papyrus-lang-vscode'; export const extensionQualifiedId = `joelday.${extensionId}`; export enum GlobalState { - PapyrusVersion = 'papyrusVersion', + PapyrusVersion = 'papyrusVersion' } +export const PDSModName = "Papyrus Debug Extension"; +export const AddressLibraryF4SEModName = "Address Library for F4SE Plugins"; +export const AddressLibrarySKSEAEModName = "Address Library for SKSE Plugins (AE)"; +export const AddressLibrarySKSEModName = "Address Library for SKSE Plugins"; + diff --git a/src/papyrus-lang-vscode/src/debugger/DebugSupportInstallService.ts b/src/papyrus-lang-vscode/src/debugger/DebugSupportInstallService.ts index a07d8506..fe923b5b 100644 --- a/src/papyrus-lang-vscode/src/debugger/DebugSupportInstallService.ts +++ b/src/papyrus-lang-vscode/src/debugger/DebugSupportInstallService.ts @@ -13,6 +13,7 @@ import * as fs from 'fs'; import { promisify } from 'util'; import md5File from 'md5-file'; +import { PDSModName } from '../common/constants'; const exists = promisify(fs.exists); const copyFile = promisify(fs.copyFile); @@ -27,8 +28,8 @@ export enum DebugSupportInstallState { } export interface IDebugSupportInstallService { - getInstallState(game: PapyrusGame): Promise; - installPlugin(game: PapyrusGame, cancellationToken?: CancellationToken): Promise; + getInstallState(game: PapyrusGame, modsDir?: string): Promise; + installPlugin(game: PapyrusGame, cancellationToken?: CancellationToken, pluginDir?: string): Promise; } @injectable() @@ -47,7 +48,9 @@ export class DebugSupportInstallService implements IDebugSupportInstallService { this._pathResolver = pathResolver; } - async getInstallState(game: PapyrusGame): Promise { + // TODO: Refactor this properly, right now it's just hacked to work with MO2LaunchDescriptor + async getInstallState(game: PapyrusGame, modsDir: string | undefined): Promise { + const config = (await this._configProvider.config.pipe(take(1)).toPromise())[game]; const client = await this._languageClientManager.getLanguageClientHost(game); const status = await client.status.pipe(take(1)).toPromise(); @@ -68,7 +71,7 @@ export class DebugSupportInstallService implements IDebugSupportInstallService { return DebugSupportInstallState.installed; } - const installedPluginPath = await this._pathResolver.getDebugPluginInstallPath(game, false); + const installedPluginPath = modsDir ? path.join(modsDir, "Plugins", PDSModName) : await this._pathResolver.getDebugPluginInstallPath(game, false); if (!installedPluginPath || !(await exists(installedPluginPath))) { return DebugSupportInstallState.notInstalled; } @@ -87,8 +90,9 @@ export class DebugSupportInstallService implements IDebugSupportInstallService { return DebugSupportInstallState.installed; } - async installPlugin(game: PapyrusGame, cancellationToken = new CancellationTokenSource().token): Promise { - const pluginInstallPath = await this._pathResolver.getDebugPluginInstallPath(game, false); + // TODO: Refactor this properly, right now it's just hacked to work with MO2LaunchDescriptor + async installPlugin(game: PapyrusGame, cancellationToken = new CancellationTokenSource().token, pluginDir: string | undefined): Promise { + const pluginInstallPath = pluginDir || await this._pathResolver.getDebugPluginInstallPath(game, false); if (!pluginInstallPath) { return false; } diff --git a/src/papyrus-lang-vscode/src/debugger/MO2Helpers.ts b/src/papyrus-lang-vscode/src/debugger/MO2Helpers.ts new file mode 100644 index 00000000..dc3c4e43 --- /dev/null +++ b/src/papyrus-lang-vscode/src/debugger/MO2Helpers.ts @@ -0,0 +1,331 @@ +import { existsSync, openSync, readFileSync, writeFileSync } from 'fs'; +import path from 'path'; +import { getScriptExtenderName } from '../PapyrusGame'; +import { PapyrusGame } from "../PapyrusGame"; + + +import * as ini from 'ini'; + +import { + getRegistryKeyForGame, + PathResolver, +} from '../common/PathResolver'; +import { + AddressLibraryF4SEModName, + AddressLibrarySKSEAEModName, + AddressLibrarySKSEModName, + PDSModName, +} from '../common/constants'; + +export interface INIData{ + [key: string]: any; +} +export enum ModEnabledState { + unmanaged = '*', + enabled = '+', + disabled = '-', + unknown = '?', +} +export class ModListItem { + public name: string = ''; + public enabled: ModEnabledState = ModEnabledState.unmanaged; + constructor(name: string, enabled: ModEnabledState) { + this.name = name; + this.enabled = enabled; + } +} + + + +// path helpers +// TODO: Move these to pathresolver +/** + * Will return the path to the plugins folder for the given game relative to the game's data folder + * + * + * @param game fallout4 or skyrimse + * @returns + */ +export function getRelativePluginPath(game: PapyrusGame) { + // TODO: make it not use this + return PathResolver._getModMgrExtenderPluginRelativePath(game); +} + +export function getGameIniName(game: PapyrusGame): string { + return game == PapyrusGame.fallout4 ? 'fallout4.ini' :'skyrim.ini'; +} + +export function getGameIniPath(profilePath: string, game: PapyrusGame): string { + let iniName = getGameIniName(game); + return path.join(profilePath, iniName); +} + +// TODO: Refactor these "Find" functions into PathResolver +export function GetGlobalGameSavedDataFolder(game: PapyrusGame){ + if (process.env.HOMEPATH === undefined) { + return undefined; + } + return path.join(process.env.HOMEPATH, "My Games", getRegistryKeyForGame(game)); +} + +export function GetLocalAppDataFolder(){ + return process.env.LOCALAPPDATA; +} + +export async function FindMO2InstanceIniPath(modsFolder: string, MO2EXEPath: string, instanceName: string | undefined){ + // check if the instance ini is in the same folder as the mods folder + let instanceIniPath = path.join(path.dirname(modsFolder), 'ModOrganizer.ini'); + if (!existsSync(instanceIniPath)) { + // check if it's a portable instance + let portableSigil = path.join(path.dirname(MO2EXEPath), 'portable.txt'); + // if it's not a portable instance, check appdata + if(!existsSync(portableSigil)){ + if (!instanceName) { + return undefined; + } + let appdata = GetLocalAppDataFolder(); + if (appdata === undefined) { + return undefined; + } + instanceIniPath = path.join(appdata, 'ModOrganizer', instanceName, 'ModOrganizer.ini'); + } else { + if (instanceName === "portable") { + // we shouldn't be here + throw new Error("FUCK!"); + } + // portable instance, instance ini should be in the same dir as the exe + instanceIniPath = path.join(path.dirname(MO2EXEPath), 'ModOrganizer.ini'); + } + } + if (!existsSync(instanceIniPath)) { + return undefined; + } + + return instanceIniPath; +} + +export async function FindMO2ProfilesFolder(modsFolder: string, MO2InstanceIniData : INIData): Promise { + + let parentFolder = path.dirname(modsFolder); + let profilesFolder = path.join(parentFolder, 'profiles'); + if (existsSync(profilesFolder)) { + return profilesFolder; + } + if (!MO2InstanceIniData) { + return undefined; + } + // if it's not there, then we have to check the Mod Organizer 2 instance ini file + profilesFolder = MO2InstanceIniData.Settings.profiles_directory; + if (!profilesFolder) { + return undefined; + } + if (profilesFolder.startsWith('%BASE_DIR%')) { + profilesFolder = path.join(parentFolder, profilesFolder.substring(10).trim()); + } else if (!path.isAbsolute(profilesFolder)) { + profilesFolder = path.join(parentFolder, profilesFolder); + } + if (existsSync(profilesFolder)) { + return profilesFolder; + } + return undefined; +} +function getAddressLibNames(game: PapyrusGame) { + if (game === PapyrusGame.fallout4) { + return [AddressLibraryF4SEModName]; + } + return [AddressLibrarySKSEModName, AddressLibrarySKSEAEModName]; +} + +// export async function FindMO2ProfileFolder(profileName: string, modFolder: string, MO2InstanceIniData : INIData ): Promise { +// let profilesFolder = await FindMO2ProfilesFolder(modFolder, MO2InstanceIniData); +// if (profilesFolder === undefined) { +// return undefined; +// } +// let profileFolder = path.join(profilesFolder, profileName); + +// if (existsSync(profileFolder)) { +// return profileFolder; +// } +// return undefined; +// } + +export async function ParseIniFile(IniPath: string): Promise { + let IniText = readFileSync(IniPath, 'utf-8'); + if (!IniText) { + return undefined; + } + let IniObject = ini.parse(readFileSync(IniPath, 'utf-8')); + return IniObject as INIData; +} + +export function CheckIfDebuggingIsEnabledInIni(skyrimIni: INIData) { + return ( + skyrimIni.Papyrus.bLoadDebugInformation === 1 && + skyrimIni.Papyrus.bEnableTrace === 1 && + skyrimIni.Papyrus.bEnableLogging === 1 + ); +} + +export function TurnOnDebuggingInIni(skyrimIni: INIData) { + const _ini = skyrimIni; + _ini.Papyrus.bLoadDebugInformation = 1; + _ini.Papyrus.bEnableTrace = 1; + _ini.Papyrus.bEnableLogging = 1; + return _ini; +} + + +/** Parse modlist.txt file contents from Mod Organizer 2 + * + * The format is: + * comments are prefixed with '#' + * mod names are the names of their directories in the mods folder + * enabled mods are prefixed with "+" and disabled mods are prefixed with "-" + * Unmanaged mods (e.g. Game DLC) are prefixed with "*" + * The mods are loaded in order. + * Any mods listed earlier will overwrite files in mods listed later. + * + * They appear in the ModOrganizer gui in reverse order (i.e. the last mod in the file is the first mod in the gui) + * + * Example of a modlist.txt file: + * ```none + * # This file was automatically generated by Mod Organizer. + * +Unofficial Skyrim Special Edition Patch + * -SkyUI + * +Immersive Citizens - AI Overhaul + * *DLC: Automatron + * +Immersive Citizens - OCS patch + * -Auto Loot SE + * *DLC: Far Harbor + * *DLC: Contraptions Workshop + * ``` + * This function returns an array of mod names in the order they appear in the file + */ +export function ParseModListText(modListContents: string): ModListItem[] { + let modlist = new Array(); + const modlistLines = modListContents.split('\n'); + for (let line of modlistLines) { + if (line.charAt(0) === '#' || line === '') { + continue; + } + let indic = line.charAt(0); + let modName = line.substring(1); + let modEnabledState: ModEnabledState | undefined = undefined; + switch (indic) { + case '+': + modEnabledState = ModEnabledState.enabled; + break; + case '-': + modEnabledState = ModEnabledState.disabled; + break; + case '*': + modEnabledState = ModEnabledState.unmanaged; + break; + } + if (modEnabledState === undefined) { + // skip this line + continue; + } + modlist.push(new ModListItem(modName, modEnabledState)); + } + return modlist; +} + +export async function ParseModListFile(modlistPath: string): Promise { + // create an ordered map of mod names to their enabled state + const modlistContents = readFileSync(modlistPath, 'utf8'); + if (!modlistContents) { + return undefined; + } + return ParseModListText(modlistContents); +} + +export async function WriteChangesToIni(gameIniPath: string, skyrimIni: INIData) { + const file = openSync(gameIniPath, 'w'); + if (!file) { + return false; + } + writeFileSync(file, ini.stringify(skyrimIni)); + return false; +} + +// parse moshortcut URI +export function parseMoshortcutURI(moshortcutURI: string): { instanceName: string; exeName: string } { + let moshortcutparts = moshortcutURI.replace('moshortcut://', '').split(':'); + let instanceName = moshortcutparts[0] || 'portable'; + let exeName = moshortcutparts[1]; + return { instanceName, exeName }; +} + +export function checkPDSModExistsAndEnabled(modlist: Array) { + return modlist.findIndex((mod) => mod.name === PDSModName && mod.enabled === ModEnabledState.enabled) !== -1; +} + +export function checkAddressLibraryExistsAndEnabled(modlist: Array, game: PapyrusGame) { + if (game === PapyrusGame.skyrimSpecialEdition) { + //TODO: check for the current version of skyrim SE' + // Right now, we just ensure both versions are installed + return ( + modlist.findIndex( + (mod) => mod.name === AddressLibrarySKSEModName && mod.enabled === ModEnabledState.enabled + ) !== -1 && + modlist.findIndex( + (mod) => mod.name === AddressLibrarySKSEAEModName && mod.enabled === ModEnabledState.enabled + ) !== -1 + ); + } else if (game === PapyrusGame.fallout4) { + return ( + modlist.findIndex( + (mod) => mod.name === AddressLibraryF4SEModName && mod.enabled === ModEnabledState.enabled + ) !== -1 + ); + } + return modlist.findIndex((mod) => mod.name === PDSModName && mod.enabled === ModEnabledState.enabled) !== -1; +} + +export function AddModToBeginningOfModList(p_modlist: Array, mod: ModListItem) { + let modlist = p_modlist; + // check if the mod is already in the modlist + let modIndex = modlist.findIndex( + (m) => m.name === mod.name + ); + if (modIndex !== -1) { + // if the mod is already in the modlist, remove it + modlist = modlist.splice(modIndex, 1); + } + + modlist = [mod].concat(modlist); + return modlist; +} + +export function AddRequiredModsToModList(p_modlist: Array, game: PapyrusGame) { + // add the debug adapter to the modlist + let modlist = p_modlist; + let debugAdapterMod = new ModListItem(PDSModName, ModEnabledState.enabled); + + let addressLibraryMods = getAddressLibNames(game).map(d => new ModListItem(d, ModEnabledState.enabled)); + + // ensure address libs load before debug adapter by putting them after the debug adapter in the modlist + modlist = AddModToBeginningOfModList(modlist, debugAdapterMod); + modlist = addressLibraryMods.reduce((modlist, mod) => + AddModToBeginningOfModList(modlist, mod), modlist + ); + return modlist; +} + +export function ModListToText(modlist: Array) { + let modlistText = '# This file was automatically generated by Mod Organizer.'; + for (let mod of modlist) { + modlistText += mod.enabled + mod.name + '\n'; + } + return modlistText; +} + +export function WriteChangesToModListFile(modlistPath: string, modlist: Array) { + let modlistContents = ModListToText(modlist); + if (!openSync(modlistPath, 'w')) { + return false; + } + writeFileSync(modlistPath, modlistContents); + return true; +} diff --git a/src/papyrus-lang-vscode/src/debugger/MO2LaunchDescriptorFactory.ts b/src/papyrus-lang-vscode/src/debugger/MO2LaunchDescriptorFactory.ts new file mode 100644 index 00000000..d33341f0 --- /dev/null +++ b/src/papyrus-lang-vscode/src/debugger/MO2LaunchDescriptorFactory.ts @@ -0,0 +1,482 @@ +import { existsSync, fstat, openSync, readFileSync, writeFileSync } from 'fs'; +import * as ini from 'ini'; +import { PapyrusGame } from '../PapyrusGame'; +import { MO2Config } from './PapyrusDebugSession'; +import { getExecutableNameForGame, IPathResolver, PathResolver } from '../common/PathResolver'; +import { PDSModName } from '../common/constants'; + +import { IExtensionConfigProvider } from '../ExtensionConfigProvider'; + +import { inject, injectable } from 'inversify'; +import path from 'path'; +import { + getGameIniPath, + FindMO2ProfilesFolder, + ParseIniFile, + CheckIfDebuggingIsEnabledInIni, + TurnOnDebuggingInIni, + WriteChangesToIni, + ParseModListFile, + checkPDSModExistsAndEnabled, + checkAddressLibraryExistsAndEnabled, + parseMoshortcutURI, + AddRequiredModsToModList, + WriteChangesToModListFile, + ModListItem, + INIData, + FindMO2InstanceIniPath, + GetGlobalGameSavedDataFolder, + getRelativePluginPath, + getGameIniName, +} from './MO2Helpers'; +import { IDebugSupportInstallService, DebugSupportInstallState } from './DebugSupportInstallService'; + +export enum MO2LaunchConfigurationState { + Ready = 0, + PDSNotInstalled = 1 >> 0, + PDSIncorrectVersion = 1 >> 1, + AddressLibraryNotInstalled = 1 >> 2, + GameIniNotSetupForDebugging = 1 >> 4, + PDSModNotEnabledInModList = 1 >> 5, + AddressLibraryModNotEnabledInModList = 1 >> 6, +} + +export interface MO2ProfileData { + name: string; + folderPath: string; + /** + * Path to the ini file that contains the settings for this profile + * Should always be present in profile folder and should always be named "settings.ini" + * @type {string} + */ + settingsIniPath: string; + settingsIniData: INIData; + /** + * Path to the txt file that contains the mod list for this profile + * Should always be present in profile folder and should always be named "modlist.txt" + * @type {string} + */ + modListPath: string; + modListData: ModListItem[]; + /** + * Path to the ini file that contains the Skyrim or Fallout 4 settings. + * Depending if the profile has local settings, this is either present in the profile folder or in the global save game folder. + * Should alays be named "Skyrim.ini" or "Fallout4.ini" + * @type {string} + */ + gameIniPath: string; + gameIniData: INIData; +} +export interface MO2InstanceData { + name: string; + modsFolder: string; + profilesFolder: string; + selectedProfileData: MO2ProfileData; +} +export function GetErrorMessageFromState(state: MO2LaunchConfigurationState): string { + let errorMessages = new Array(); + switch (state) { + case MO2LaunchConfigurationState.Ready: + return 'Ready'; + case state & MO2LaunchConfigurationState.PDSNotInstalled: + errorMessages.push('Papyrus Debug Support is not installed'); + case MO2LaunchConfigurationState.PDSIncorrectVersion: + errorMessages.push('Papyrus Debug Support is not the correct version'); + case MO2LaunchConfigurationState.AddressLibraryNotInstalled: + errorMessages.push('Address Library is not installed'); + case MO2LaunchConfigurationState.GameIniNotSetupForDebugging: + errorMessages.push('Game ini is not setup for debugging'); + case MO2LaunchConfigurationState.PDSModNotEnabledInModList: + errorMessages.push('Papyrus Debug Support mod is not enabled in the mod list'); + case MO2LaunchConfigurationState.AddressLibraryModNotEnabledInModList: + errorMessages.push('Address Library mod is not enabled in the mod list'); + default: + } + const errMsg = 'The following errors were found: \n -' + errorMessages.join('\n -'); + return errMsg; +} + +@injectable() +export class MO2LaunchConfigurationFactory { + private readonly _debugSupportInstallService: IDebugSupportInstallService; + + constructor(@inject(IDebugSupportInstallService) debugSupportInstallService: IDebugSupportInstallService) { + this._debugSupportInstallService = debugSupportInstallService; + } + // TODO: After testing, make these private + public static async populateMO2ProfileData( + name: string, + profileFolder: string, + game: PapyrusGame + ): Promise { + if (!existsSync(profileFolder)) { + throw new Error(`Could not find the profile folder ${profileFolder}}`); + } + + // settings.ini should always be present in profiles + const settingsIniPath = path.join(profileFolder, 'settings.ini'); + if (!existsSync(settingsIniPath)) { + throw new Error(`Could not find the settings.ini file in ${profileFolder}}`); + } + const settingsIniData = await ParseIniFile(settingsIniPath); + if ( + !settingsIniData || + settingsIniData.General === undefined || + settingsIniData.General.LocalSettings === undefined + ) { + throw new Error(`MO2 profile Settings ini file ${settingsIniPath} is not parsable`); + } + + // Game ini paths for MO2 are different depending on whether the profile has local settings or not + // if [General] LocalSettings=false, then the game ini is in the global game save folder + // if [General] LocalSettings=true, then the game ini is in the profile folder + const gameIniName = getGameIniName(game); + let gameIniPath: string; + if (settingsIniData.General.LocalSettings === false) { + // We don't have local game ini settings, so we need to use the global ones + let gameSaveDir = GetGlobalGameSavedDataFolder(game); + if (!gameSaveDir) { + throw new Error(`Could not find the Global ${game} save directory`); + } + if (!existsSync(gameSaveDir)) { + throw new Error( + `MO2 profile does not have local game INI settings, but could not find the global game save directory at ${gameSaveDir}` + ); + } + gameIniPath = path.join(gameSaveDir, gameIniName); + if (!existsSync(gameIniPath)) { + throw new Error( + `MO2 profile does not have local game INI settings, but could not find the global game ${game} ini @ ${gameIniPath} (Try running the game once to generate the ini file)` + ); + } + } else { + gameIniPath = getGameIniPath(profileFolder, game); + if (!existsSync(gameIniPath)) { + throw new Error( + `MO2 profile has local game INI settings, but could not find the local ${game} ini @ ${gameIniPath}` + ); + } + } + + if (!existsSync(gameIniPath)) { + throw new Error(`Could not find the skyrim.ini file @ ${gameIniPath}`); + } + const gameIniData = await ParseIniFile(gameIniPath); + if (!gameIniData) { + throw new Error(`Game ini file is not parsable`); + } + const ModsListPath = path.join(profileFolder, 'modlist.txt'); + if (!existsSync(ModsListPath)) { + throw new Error(`Could not find the modlist.txt file`); + } + const ModsListData = await ParseModListFile(ModsListPath); + if (!ModsListData) { + throw new Error(`Mod list file is not parsable`); + } + return { + name: name, + folderPath: profileFolder, + settingsIniPath: settingsIniPath, + settingsIniData: settingsIniData, + modListPath: ModsListPath, + modListData: ModsListData, + gameIniPath: gameIniPath, + gameIniData: gameIniData, + } as MO2ProfileData; + } + + public static async populateMO2InstanceData(mo2Config: MO2Config, game: PapyrusGame): Promise { + // taken care of by debug config provider + const modsFolder = mo2Config.modsFolder; + if (!mo2Config.modsFolder) { + throw new Error(`Mod directory path is not set`); + } + // TODO: Have the debug config provider check this before we get here + const { instanceName, exeName } = parseMoshortcutURI(mo2Config.shortcut); + if (!instanceName || !exeName) { + throw new Error(`Could not parse the shortcut URI ${mo2Config.shortcut}}`); + } + + let profilesFolder = mo2Config.profilesFolder; + // TODO: Consider moving this to the DebugConfigProvider + if (!profilesFolder) { + // Try the parent folder of the mods folder + profilesFolder = path.join(mo2Config.modsFolder, '..', 'profiles'); + // If it's not there, then we have to parse the MO2 ini to find the profiles folder + if (!existsSync(profilesFolder)) { + // Instance directories are always where ModOrganizer.ini is located + const InstanceIniPath = await FindMO2InstanceIniPath( + mo2Config.modsFolder, + mo2Config.MO2EXEPath, + mo2Config.profile + ); + if (!InstanceIniPath || !existsSync(InstanceIniPath)) { + throw new Error(`Profiles Folder not set, but could not find the instance.ini file`); + } + + const InstanceDirectory = path.dirname(InstanceIniPath); + + const InstanceIniData = await ParseIniFile(InstanceIniPath); + if (!InstanceIniData) { + throw new Error( + `Profiles Folder not set, but instance ini file at ${InstanceIniPath} is not parsable` + ); + } + profilesFolder = await FindMO2ProfilesFolder(mo2Config.modsFolder, InstanceIniData); + if (!profilesFolder) { + throw new Error( + `Profiles Folder not set, but could not find the "profiles" folder in the instance directory ${InstanceDirectory}` + ); + } + } + } + if (existsSync(profilesFolder)) { + throw new Error(`Could not find the "profiles" folder: ${profilesFolder}`); + } + + // taken care of by debug config provider + const profileName = mo2Config.profile; + if (!profileName) { + throw new Error(`Profile name is not set`); + } + + const profileFolder = path.join(profilesFolder, profileName); + if (!existsSync(profileFolder)) { + throw new Error(`Could not find profile folder ${profileName} in ${profilesFolder}`); + } + + let selectedProfileData: MO2ProfileData; + try { + selectedProfileData = await this.populateMO2ProfileData(profileName, profileFolder, game); + } catch (error) { + throw new Error(`Could not populate the profile data: ${error}`); + } + + return { + name: instanceName, + modsFolder: modsFolder, + profilesFolder: profilesFolder, + selectedProfileData: selectedProfileData, + } as MO2InstanceData; + } + + public static async populateMO2LaunchConfiguration( + mo2Config: MO2Config, + game: PapyrusGame + ): Promise { + // taken care of by debug config provider + if (!mo2Config.modsFolder) { + throw new Error(`Mod directory path is not set`); + } + if (!existsSync(mo2Config.modsFolder)) { + throw new Error(`Mod directory path does not exist`); + } + + // TODO: make the debug config provider do this + if (!mo2Config.profile) { + throw new Error(`Profile is not set`); + } + + let { instanceName, exeName } = parseMoshortcutURI(mo2Config.shortcut); + if (!instanceName || !exeName) { + throw new Error(`Could not parse the shortcut URI`); + } + + let MO2EXEPath = mo2Config.MO2EXEPath; + if (!MO2EXEPath || !existsSync(MO2EXEPath)) { + throw new Error(`Could not find the Mod Organizer 2 executable path`); + } + let instanceData: MO2InstanceData; + try { + instanceData = await this.populateMO2InstanceData(mo2Config, game); + } catch (error) { + throw new Error(`Could not populate the instance data: ${error}`); + } + const args = mo2Config.args || []; + return { + exeName, + MO2EXEPath, + args, + game, + instanceData, + } as IMO2LauncherDescriptor; + } + + public async createMO2LaunchDecriptor(mo2Config: MO2Config, game: PapyrusGame): Promise { + let idescriptor: IMO2LauncherDescriptor; + try { + idescriptor = await MO2LaunchConfigurationFactory.populateMO2LaunchConfiguration(mo2Config, game); + } catch (error) { + throw new Error(`Could not create the launch configuration: ${error}`); + } + + return new MO2LaunchDescriptor( + idescriptor.exeName, + idescriptor.MO2EXEPath, + idescriptor.args, + idescriptor.game, + idescriptor.instanceData, + this._debugSupportInstallService + ); + } +} + +export interface LaunchCommand { + command: string; + args: string[]; +} + +export interface IMO2LauncherDescriptor { + exeName: string; + MO2EXEPath: string; + args: string[]; + game: PapyrusGame; + instanceData: MO2InstanceData; + checkIfDebuggerConfigured(): Promise; + fixDebuggerConfiguration(): Promise; + getLaunchCommand(): LaunchCommand; +} + +export class MO2LaunchDescriptor implements IMO2LauncherDescriptor { + public readonly exeName: string; + public readonly MO2EXEPath: string; + public readonly args: string[]; + public readonly game: PapyrusGame; + public readonly instanceData: MO2InstanceData; + + // TODO: Refactor this to not use this + private readonly _debugSupportInstallService: IDebugSupportInstallService; + + constructor( + exeName: string, + MO2EXEPath: string, + args: string[], + game: PapyrusGame, + instanceData: MO2InstanceData, + debugSupportInstallService: IDebugSupportInstallService + ) { + this.exeName = exeName; + this.MO2EXEPath = MO2EXEPath; + this.args = args; + this.game = game; + this.instanceData = instanceData; + this._debugSupportInstallService = debugSupportInstallService; + } + + public async checkIfDebuggerConfigured(): Promise { + let ret: MO2LaunchConfigurationState = + (await this.checkIfModsArePresent()) | + (await this.checkIfGameIniIsCorrectlyConfigured()) | + (await this.checkIfMO2IsCorrectlyConfigured()); + return ret; + } + + public async fixDebuggerConfiguration(): Promise { + let state = await this.checkIfDebuggerConfigured(); + while (state !== MO2LaunchConfigurationState.Ready) { + switch (state) { + case MO2LaunchConfigurationState.Ready: + break; + case state & MO2LaunchConfigurationState.PDSNotInstalled: + case state & MO2LaunchConfigurationState.PDSIncorrectVersion: + let relativePluginPath = getRelativePluginPath(this.game); + if ( + !(await this._debugSupportInstallService.installPlugin( + this.game, + undefined, + path.join(this.instanceData.modsFolder, PDSModName, relativePluginPath) + )) + ) { + return false; + } + state &= ~MO2LaunchConfigurationState.PDSNotInstalled; + state &= ~MO2LaunchConfigurationState.PDSIncorrectVersion; + break; + case state & MO2LaunchConfigurationState.GameIniNotSetupForDebugging: + const inidata = await ParseIniFile(this.instanceData.selectedProfileData.gameIniPath); + if (!inidata) { + return false; + } + const newGameIni = TurnOnDebuggingInIni(inidata); + if (!WriteChangesToIni(this.instanceData.selectedProfileData.gameIniPath, newGameIni)) { + return false; + } + this.instanceData.selectedProfileData.gameIniData = newGameIni; + state &= ~MO2LaunchConfigurationState.GameIniNotSetupForDebugging; + break; + case state & MO2LaunchConfigurationState.PDSModNotEnabledInModList: + case state & MO2LaunchConfigurationState.AddressLibraryModNotEnabledInModList: + const modList = await ParseModListFile(this.instanceData.selectedProfileData.modListPath); + if (!modList) { + return false; + } + const newmodList = AddRequiredModsToModList(modList, this.game); + if (!WriteChangesToModListFile(this.instanceData.selectedProfileData.modListPath, modList)) { + return false; + } + this.instanceData.selectedProfileData.modListData = newmodList; + state &= ~MO2LaunchConfigurationState.PDSModNotEnabledInModList; + state &= ~MO2LaunchConfigurationState.AddressLibraryModNotEnabledInModList; + break; + default: + // shouldn't reach here + throw new Error(`Unknown state in fixDebuggerConfiguration`); + } + if (state === MO2LaunchConfigurationState.Ready) { + break; + } + } + return true; + } + public getLaunchCommand(): LaunchCommand { + let command = this.MO2EXEPath; + let cmdargs = ['-p', this.instanceData.selectedProfileData.name]; + if (this.instanceData.name !== 'portable') { + cmdargs = cmdargs.concat(['-i', this.instanceData.name]); + } + cmdargs.concat('-e', this.exeName); + if (this.args) { + cmdargs = cmdargs.concat(['-a'].concat(this.args)); + } + return { + command: command, + args: cmdargs, + } as LaunchCommand; + } + + public async checkIfModsArePresent(): Promise { + // TODO: Change this to not have to read global state + let result = await this._debugSupportInstallService.getInstallState(this.game, this.instanceData.modsFolder); + if (result !== DebugSupportInstallState.installed) { + if (result === DebugSupportInstallState.notInstalled) { + return MO2LaunchConfigurationState.PDSNotInstalled; + } + if (result === DebugSupportInstallState.incorrectVersion) { + return MO2LaunchConfigurationState.PDSIncorrectVersion; + } else { + // TODO : FIX THIS + throw new Error(`Unknown result from getInstallState`); + } + } + return MO2LaunchConfigurationState.Ready; + } + + async checkIfGameIniIsCorrectlyConfigured(): Promise { + if (!CheckIfDebuggingIsEnabledInIni(this.instanceData.selectedProfileData.gameIniData)) { + return MO2LaunchConfigurationState.GameIniNotSetupForDebugging; + } + return MO2LaunchConfigurationState.Ready; + } + + // Check if the MO2 modlist has the PDS mod and the Address Library mod enabled + async checkIfMO2IsCorrectlyConfigured(): Promise { + let ret: MO2LaunchConfigurationState = MO2LaunchConfigurationState.Ready; + if (!checkPDSModExistsAndEnabled(this.instanceData.selectedProfileData.modListData)) { + ret |= MO2LaunchConfigurationState.PDSModNotEnabledInModList; + } + if (!checkAddressLibraryExistsAndEnabled(this.instanceData.selectedProfileData.modListData, this.game)) { + ret |= MO2LaunchConfigurationState.AddressLibraryModNotEnabledInModList; + } + return ret; + } +} From bb51560976384c3d950c4c313bd9b4e830d858f7 Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Sun, 1 Oct 2023 16:13:57 -0700 Subject: [PATCH 03/15] Refactor non-vscode funcs to ease testing --- .../src/CreationKitInfoProvider.ts | 4 +- src/papyrus-lang-vscode/src/PapyrusGame.ts | 66 ++++++++----------- src/papyrus-lang-vscode/src/Utilities.ts | 20 +++--- .../src/VsCodeUtilities.ts | 13 ++++ src/papyrus-lang-vscode/src/WorkspaceGame.ts | 48 ++++++++++++++ .../src/common/PathResolver.ts | 44 ++----------- .../src/debugger/DebugLauncherService.ts | 3 - .../debugger/DebugSupportInstallService.ts | 2 +- .../src/debugger/MO2Helpers.ts | 9 +-- .../debugger/MO2LaunchDescriptorFactory.ts | 5 +- .../PapyrusDebugAdapterDescriptorFactory.ts | 4 +- .../features/LanguageServiceStatusItems.ts | 2 +- .../src/features/PyroTaskProvider.ts | 3 +- .../commands/AttachDebuggerCommand.ts | 2 +- .../src/features/commands/GameCommandBase.ts | 2 +- .../commands/GenerateProjectCommand.ts | 2 +- .../commands/InstallDebugSupportCommand.ts | 5 +- .../commands/SearchCreationKitWikiCommand.ts | 2 +- .../projects/ProjectsTreeDataProvider.ts | 2 +- .../src/server/LanguageClient.ts | 2 +- .../src/server/LanguageClientHost.ts | 10 +-- .../src/server/LanguageClientManager.ts | 2 +- 22 files changed, 128 insertions(+), 124 deletions(-) create mode 100644 src/papyrus-lang-vscode/src/VsCodeUtilities.ts create mode 100644 src/papyrus-lang-vscode/src/WorkspaceGame.ts diff --git a/src/papyrus-lang-vscode/src/CreationKitInfoProvider.ts b/src/papyrus-lang-vscode/src/CreationKitInfoProvider.ts index 0fd35a53..344f6cc6 100644 --- a/src/papyrus-lang-vscode/src/CreationKitInfoProvider.ts +++ b/src/papyrus-lang-vscode/src/CreationKitInfoProvider.ts @@ -1,9 +1,9 @@ import { interfaces, inject, injectable } from 'inversify'; -import { PapyrusGame, getGames } from './PapyrusGame'; +import { PapyrusGame, getGames, getDevelopmentCompilerFolderForGame } from "./PapyrusGame"; import { IExtensionConfigProvider } from './ExtensionConfigProvider'; import { Observable, combineLatest } from 'rxjs'; import { map, mergeMap, shareReplay } from 'rxjs/operators'; -import { IPathResolver, getDevelopmentCompilerFolderForGame } from './common/PathResolver'; +import { IPathResolver } from './common/PathResolver'; import { inDevelopmentEnvironment } from './Utilities'; import * as path from 'path'; import * as ini from 'ini'; diff --git a/src/papyrus-lang-vscode/src/PapyrusGame.ts b/src/papyrus-lang-vscode/src/PapyrusGame.ts index b6d5c53e..6b275a72 100644 --- a/src/papyrus-lang-vscode/src/PapyrusGame.ts +++ b/src/papyrus-lang-vscode/src/PapyrusGame.ts @@ -1,20 +1,8 @@ -import * as fs from 'fs'; -import { promisify } from 'util'; - -import { xml2js } from 'xml-js'; - -import { workspace, Uri, RelativePattern } from 'vscode'; - -import { PyroGameToPapyrusGame } from './features/PyroTaskDefinition'; - -const readFile = promisify(fs.readFile); - export enum PapyrusGame { fallout4 = 'fallout4', skyrim = 'skyrim', skyrimSpecialEdition = 'skyrimSpecialEdition', } - const displayNames = new Map([ [PapyrusGame.fallout4, 'Fallout 4'], [PapyrusGame.skyrim, 'Skyrim'], @@ -57,40 +45,38 @@ export function getGames(): PapyrusGame[] { return (Object.keys(PapyrusGame) as (keyof typeof PapyrusGame)[]).map((k) => PapyrusGame[k]); } -export async function getWorkspaceGameFromProjects(ppjFiles: Uri[]): Promise { - let game: string | undefined = undefined; - if (!ppjFiles) { - return undefined; - } - - for (const ppjFile of ppjFiles) { - game = await getWorkspaceGameFromProjectFile(ppjFile.fsPath); - if (game) { - break; - } +export function getRegistryKeyForGame(game: PapyrusGame) { + switch (game) { + case PapyrusGame.fallout4: + return 'Fallout4'; + case PapyrusGame.skyrim: + return 'Skyrim'; + case PapyrusGame.skyrimSpecialEdition: + return 'Skyrim Special Edition'; } +} - if (!game || !PyroGameToPapyrusGame[game as keyof typeof PyroGameToPapyrusGame]) { - return undefined; +export function getDevelopmentCompilerFolderForGame(game: PapyrusGame) { + switch (game) { + case PapyrusGame.fallout4: + return 'fallout4'; + case PapyrusGame.skyrim: + return 'does-not-exist'; + case PapyrusGame.skyrimSpecialEdition: + return 'skyrim'; } - - return PyroGameToPapyrusGame[game as keyof typeof PyroGameToPapyrusGame] as unknown as PapyrusGame; } -export async function getWorkspaceGameFromProjectFile(projectFile: string): Promise { - const xml = await readFile(projectFile, { encoding: 'utf-8' }); - // TODO: Annoying type cast here: - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const results = xml2js(xml, { compact: true, trim: true }) as Record; - - return results['PapyrusProject']['_attributes']['Game']; +export function getDefaultFlagsFileNameForGame(game: PapyrusGame) { + return game === PapyrusGame.fallout4 ? 'Institute_Papyrus_Flags.flg' : 'TESV_Papyrus_Flags.flg'; } -export async function getWorkspaceGame(): Promise { - if (!workspace.workspaceFolders) { - return undefined; - } +const executableNames = new Map([ + [PapyrusGame.skyrim, 'Skyrim.exe'], + [PapyrusGame.fallout4, 'Fallout4.exe'], + [PapyrusGame.skyrimSpecialEdition, 'SkyrimSE.exe'], +]); - const ppjFiles: Uri[] = await workspace.findFiles(new RelativePattern(workspace.workspaceFolders[0], '**/*.ppj')); - return getWorkspaceGameFromProjects(ppjFiles); +export function getExecutableNameForGame(game: PapyrusGame) { + return executableNames.get(game)!; } diff --git a/src/papyrus-lang-vscode/src/Utilities.ts b/src/papyrus-lang-vscode/src/Utilities.ts index 51278770..9472b973 100644 --- a/src/papyrus-lang-vscode/src/Utilities.ts +++ b/src/papyrus-lang-vscode/src/Utilities.ts @@ -4,9 +4,7 @@ import { promisify } from 'util'; import procList from 'ps-list'; -import { CancellationTokenSource } from 'vscode'; -import { PapyrusGame } from './PapyrusGame'; -import { getExecutableNameForGame } from './common/PathResolver'; +import { getExecutableNameForGame, PapyrusGame } from "./PapyrusGame"; import { isNativeError } from 'util/types'; @@ -33,14 +31,16 @@ export async function getGameIsRunning(game: PapyrusGame) { return processList.some((p) => p.name.toLowerCase() === getExecutableNameForGame(game).toLowerCase()); } -export async function waitWhile( - func: () => Promise, - cancellationToken = new CancellationTokenSource().token, - pollingFrequencyMs = 1000 -) { - while ((await func()) && !cancellationToken.isCancellationRequested) { - await delayAsync(pollingFrequencyMs); +export async function getGamePIDs(game: PapyrusGame): Promise> { + const processList = await procList(); + + const gameProcesses = processList.filter((p) => p.name.toLowerCase() === getExecutableNameForGame(game).toLowerCase()); + + if (gameProcesses.length === 0) { + return []; } + + return gameProcesses.map((p) => p.pid); } export function inDevelopmentEnvironment() { diff --git a/src/papyrus-lang-vscode/src/VsCodeUtilities.ts b/src/papyrus-lang-vscode/src/VsCodeUtilities.ts new file mode 100644 index 00000000..9f85b8eb --- /dev/null +++ b/src/papyrus-lang-vscode/src/VsCodeUtilities.ts @@ -0,0 +1,13 @@ +import { CancellationTokenSource } from 'vscode'; +import { delayAsync } from './Utilities'; + + +export async function waitWhile( + func: () => Promise, + cancellationToken = new CancellationTokenSource().token, + pollingFrequencyMs = 1000 +) { + while ((await func()) && !cancellationToken.isCancellationRequested) { + await delayAsync(pollingFrequencyMs); + } +} diff --git a/src/papyrus-lang-vscode/src/WorkspaceGame.ts b/src/papyrus-lang-vscode/src/WorkspaceGame.ts new file mode 100644 index 00000000..f6a45e0d --- /dev/null +++ b/src/papyrus-lang-vscode/src/WorkspaceGame.ts @@ -0,0 +1,48 @@ +import * as fs from 'fs'; +import { promisify } from 'util'; + +import { xml2js } from 'xml-js'; + +import { workspace, Uri, RelativePattern } from 'vscode'; + +import { PyroGameToPapyrusGame } from './features/PyroTaskDefinition'; +import { PapyrusGame } from './PapyrusGame'; + +const readFile = promisify(fs.readFile); + +export async function getWorkspaceGameFromProjects(ppjFiles: Uri[]): Promise { + let game: string | undefined = undefined; + if (!ppjFiles) { + return undefined; + } + + for (let ppjFile of ppjFiles) { + game = await getWorkspaceGameFromProjectFile(ppjFile.fsPath); + if (game) { + break; + } + } + + if (!game || !PyroGameToPapyrusGame[game as keyof typeof PyroGameToPapyrusGame]) { + return undefined; + } + + return PyroGameToPapyrusGame[game as keyof typeof PyroGameToPapyrusGame] as unknown as PapyrusGame; +} + +export async function getWorkspaceGameFromProjectFile(projectFile: string): Promise { + const xml = await readFile(projectFile, { encoding: 'utf-8' }); + // TODO: Annoying type cast here: + const results = xml2js(xml, { compact: true, trim: true }) as Record; + + return results['PapyrusProject']['_attributes']['Game']; +} + +export async function getWorkspaceGame(): Promise { + if (!workspace.workspaceFolders) { + return undefined; + } + + const ppjFiles: Uri[] = await workspace.findFiles(new RelativePattern(workspace.workspaceFolders[0], "**/*.ppj")); + return getWorkspaceGameFromProjects(ppjFiles); +} \ No newline at end of file diff --git a/src/papyrus-lang-vscode/src/common/PathResolver.ts b/src/papyrus-lang-vscode/src/common/PathResolver.ts index 5d5a75eb..ab6d1b2e 100644 --- a/src/papyrus-lang-vscode/src/common/PathResolver.ts +++ b/src/papyrus-lang-vscode/src/common/PathResolver.ts @@ -9,7 +9,7 @@ import { promisify } from 'util'; import { ExtensionContext } from 'vscode'; import { IExtensionContext } from '../common/vscode/IocDecorators'; -import { PapyrusGame, getScriptExtenderName } from '../PapyrusGame'; +import { PapyrusGame, getScriptExtenderName, getRegistryKeyForGame } from "../PapyrusGame"; import { inDevelopmentEnvironment } from '../Utilities'; import { IExtensionConfigProvider, IGameConfig, IExtensionConfig } from '../ExtensionConfigProvider'; import { PDSModName } from './constants'; @@ -181,28 +181,6 @@ function getToolGameName(game: PapyrusGame): string { /*** External paths (ones that are not "ours") */ /************************************************************************* */ -export function getRegistryKeyForGame(game: PapyrusGame) { - switch (game) { - case PapyrusGame.fallout4: - return 'Fallout4'; - case PapyrusGame.skyrim: - return 'Skyrim'; - case PapyrusGame.skyrimSpecialEdition: - return 'Skyrim Special Edition'; - } -} - -export function getDevelopmentCompilerFolderForGame(game: PapyrusGame) { - switch (game) { - case PapyrusGame.fallout4: - return 'fallout4'; - case PapyrusGame.skyrim: - return 'does-not-exist'; - case PapyrusGame.skyrimSpecialEdition: - return 'skyrim'; - } -} - export async function resolveInstallPath( game: PapyrusGame, installPath: string, @@ -211,11 +189,11 @@ export async function resolveInstallPath( if (await exists(installPath)) { return installPath; } - + const regkey = getRegistryKeyForGame( + game + ); const reg = new winreg({ - key: `\\SOFTWARE\\${process.arch === 'x64' ? 'WOW6432Node\\' : ''}Bethesda Softworks\\${getRegistryKeyForGame( - game - )}`, + key: `\\SOFTWARE\\${process.arch === 'x64' ? 'WOW6432Node\\' : ''}Bethesda Softworks\\${regkey}`, }); try { @@ -235,19 +213,7 @@ export async function resolveInstallPath( return null; } -export function getDefaultFlagsFileNameForGame(game: PapyrusGame) { - return game === PapyrusGame.fallout4 ? 'Institute_Papyrus_Flags.flg' : 'TESV_Papyrus_Flags.flg'; -} -const executableNames = new Map([ - [PapyrusGame.skyrim, 'Skyrim.exe'], - [PapyrusGame.fallout4, 'Fallout4.exe'], - [PapyrusGame.skyrimSpecialEdition, 'SkyrimSE.exe'], -]); - -export function getExecutableNameForGame(game: PapyrusGame) { - return executableNames.get(game)!; -} export function pathToOsPath(pathName: string) { return path.format(path.parse(pathName)); diff --git a/src/papyrus-lang-vscode/src/debugger/DebugLauncherService.ts b/src/papyrus-lang-vscode/src/debugger/DebugLauncherService.ts index 121587f6..7baef535 100644 --- a/src/papyrus-lang-vscode/src/debugger/DebugLauncherService.ts +++ b/src/papyrus-lang-vscode/src/debugger/DebugLauncherService.ts @@ -1,12 +1,9 @@ import { inject, injectable, interfaces } from 'inversify'; import { IExtensionConfigProvider } from '../ExtensionConfigProvider'; import { CancellationToken, CancellationTokenSource, window } from 'vscode'; -import { take } from 'rxjs/operators'; import { IPathResolver } from '../common/PathResolver'; -import { getDisplayNameForGame } from '../PapyrusGame'; import { PapyrusGame } from "../PapyrusGame"; import { ILanguageClientManager } from '../server/LanguageClientManager'; -import { ClientHostStatus } from '../server/LanguageClientHost'; import { getGameIsRunning, getGamePIDs, mkdirIfNeeded } from '../Utilities'; import * as path from 'path'; diff --git a/src/papyrus-lang-vscode/src/debugger/DebugSupportInstallService.ts b/src/papyrus-lang-vscode/src/debugger/DebugSupportInstallService.ts index fe923b5b..a621e786 100644 --- a/src/papyrus-lang-vscode/src/debugger/DebugSupportInstallService.ts +++ b/src/papyrus-lang-vscode/src/debugger/DebugSupportInstallService.ts @@ -3,7 +3,7 @@ import { IExtensionConfigProvider } from '../ExtensionConfigProvider'; import { CancellationToken, CancellationTokenSource } from 'vscode'; import { take } from 'rxjs/operators'; import { IPathResolver } from '../common/PathResolver'; -import { PapyrusGame } from '../PapyrusGame'; +import { PapyrusGame } from "../PapyrusGame"; import { ILanguageClientManager } from '../server/LanguageClientManager'; import { ClientHostStatus } from '../server/LanguageClientHost'; import { mkdirIfNeeded } from '../Utilities'; diff --git a/src/papyrus-lang-vscode/src/debugger/MO2Helpers.ts b/src/papyrus-lang-vscode/src/debugger/MO2Helpers.ts index dc3c4e43..7b2352af 100644 --- a/src/papyrus-lang-vscode/src/debugger/MO2Helpers.ts +++ b/src/papyrus-lang-vscode/src/debugger/MO2Helpers.ts @@ -1,15 +1,11 @@ import { existsSync, openSync, readFileSync, writeFileSync } from 'fs'; import path from 'path'; -import { getScriptExtenderName } from '../PapyrusGame'; +import { getRegistryKeyForGame, getScriptExtenderName } from "../PapyrusGame"; import { PapyrusGame } from "../PapyrusGame"; import * as ini from 'ini'; -import { - getRegistryKeyForGame, - PathResolver, -} from '../common/PathResolver'; import { AddressLibraryF4SEModName, AddressLibrarySKSEAEModName, @@ -47,8 +43,7 @@ export class ModListItem { * @returns */ export function getRelativePluginPath(game: PapyrusGame) { - // TODO: make it not use this - return PathResolver._getModMgrExtenderPluginRelativePath(game); + return `${getScriptExtenderName(game)}/Plugins`; } export function getGameIniName(game: PapyrusGame): string { diff --git a/src/papyrus-lang-vscode/src/debugger/MO2LaunchDescriptorFactory.ts b/src/papyrus-lang-vscode/src/debugger/MO2LaunchDescriptorFactory.ts index d33341f0..194d55b1 100644 --- a/src/papyrus-lang-vscode/src/debugger/MO2LaunchDescriptorFactory.ts +++ b/src/papyrus-lang-vscode/src/debugger/MO2LaunchDescriptorFactory.ts @@ -1,12 +1,9 @@ import { existsSync, fstat, openSync, readFileSync, writeFileSync } from 'fs'; import * as ini from 'ini'; -import { PapyrusGame } from '../PapyrusGame'; +import { PapyrusGame } from "../PapyrusGame"; import { MO2Config } from './PapyrusDebugSession'; -import { getExecutableNameForGame, IPathResolver, PathResolver } from '../common/PathResolver'; import { PDSModName } from '../common/constants'; -import { IExtensionConfigProvider } from '../ExtensionConfigProvider'; - import { inject, injectable } from 'inversify'; import path from 'path'; import { diff --git a/src/papyrus-lang-vscode/src/debugger/PapyrusDebugAdapterDescriptorFactory.ts b/src/papyrus-lang-vscode/src/debugger/PapyrusDebugAdapterDescriptorFactory.ts index 07357c88..832fe4f9 100644 --- a/src/papyrus-lang-vscode/src/debugger/PapyrusDebugAdapterDescriptorFactory.ts +++ b/src/papyrus-lang-vscode/src/debugger/PapyrusDebugAdapterDescriptorFactory.ts @@ -17,8 +17,8 @@ import { getDisplayNameForGame, getScriptExtenderName, getScriptExtenderUrl, - getShortDisplayNameForGame, -} from '../PapyrusGame'; + getShortDisplayNameForGame +} from "../PapyrusGame"; import { ICreationKitInfoProvider } from '../CreationKitInfoProvider'; import { IExtensionConfigProvider } from '../ExtensionConfigProvider'; import { take } from 'rxjs/operators'; diff --git a/src/papyrus-lang-vscode/src/features/LanguageServiceStatusItems.ts b/src/papyrus-lang-vscode/src/features/LanguageServiceStatusItems.ts index 51ce6c9f..39b47bc1 100644 --- a/src/papyrus-lang-vscode/src/features/LanguageServiceStatusItems.ts +++ b/src/papyrus-lang-vscode/src/features/LanguageServiceStatusItems.ts @@ -1,6 +1,6 @@ import { ILanguageClientManager } from '../server/LanguageClientManager'; import { StatusBarItem, Disposable, window, StatusBarAlignment, TextEditor } from 'vscode'; -import { PapyrusGame, getGames, getShortDisplayNameForGame, getDisplayNameForGame } from '../PapyrusGame'; +import { PapyrusGame, getGames, getShortDisplayNameForGame, getDisplayNameForGame } from "../PapyrusGame"; import { Observable, Unsubscribable, combineLatest as combineLatest, ObservableInput, ObservedValueOf } from 'rxjs'; import { ILanguageClientHost, ClientHostStatus } from '../server/LanguageClientHost'; import { mergeMap, shareReplay } from 'rxjs/operators'; diff --git a/src/papyrus-lang-vscode/src/features/PyroTaskProvider.ts b/src/papyrus-lang-vscode/src/features/PyroTaskProvider.ts index b3219abb..51619a75 100644 --- a/src/papyrus-lang-vscode/src/features/PyroTaskProvider.ts +++ b/src/papyrus-lang-vscode/src/features/PyroTaskProvider.ts @@ -14,7 +14,8 @@ import { import { CancellationToken, Disposable } from 'vscode-jsonrpc'; import { IPyroTaskDefinition, TaskOf, PyroGameToPapyrusGame } from './PyroTaskDefinition'; -import { PapyrusGame, getWorkspaceGameFromProjects } from '../PapyrusGame'; +import { getWorkspaceGameFromProjects, getWorkspaceGame } from '../WorkspaceGame'; +import { PapyrusGame } from "../PapyrusGame"; import { IPathResolver, PathResolver, pathToOsPath } from '../common/PathResolver'; import { inject, injectable } from 'inversify'; diff --git a/src/papyrus-lang-vscode/src/features/commands/AttachDebuggerCommand.ts b/src/papyrus-lang-vscode/src/features/commands/AttachDebuggerCommand.ts index d7d342f7..54917743 100644 --- a/src/papyrus-lang-vscode/src/features/commands/AttachDebuggerCommand.ts +++ b/src/papyrus-lang-vscode/src/features/commands/AttachDebuggerCommand.ts @@ -1,7 +1,7 @@ import { injectable } from 'inversify'; import { debug } from 'vscode'; import { IPapyrusDebugConfiguration } from '../../debugger/PapyrusDebugSession'; -import { PapyrusGame, getShortDisplayNameForGame } from '../../PapyrusGame'; +import { PapyrusGame, getShortDisplayNameForGame } from "../../PapyrusGame"; import { GameCommandBase } from './GameCommandBase'; @injectable() diff --git a/src/papyrus-lang-vscode/src/features/commands/GameCommandBase.ts b/src/papyrus-lang-vscode/src/features/commands/GameCommandBase.ts index f11b0b1f..91c09c3d 100644 --- a/src/papyrus-lang-vscode/src/features/commands/GameCommandBase.ts +++ b/src/papyrus-lang-vscode/src/features/commands/GameCommandBase.ts @@ -1,6 +1,6 @@ import { injectable, unmanaged } from 'inversify'; import { commands, Disposable } from 'vscode'; -import { PapyrusGame, getGames } from '../../PapyrusGame'; +import { PapyrusGame, getGames } from "../../PapyrusGame"; @injectable() // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/papyrus-lang-vscode/src/features/commands/GenerateProjectCommand.ts b/src/papyrus-lang-vscode/src/features/commands/GenerateProjectCommand.ts index 34c20502..54b2d93a 100644 --- a/src/papyrus-lang-vscode/src/features/commands/GenerateProjectCommand.ts +++ b/src/papyrus-lang-vscode/src/features/commands/GenerateProjectCommand.ts @@ -1,7 +1,7 @@ import { window, Uri, ExtensionContext } from 'vscode'; import { IExtensionContext } from '../../common/vscode/IocDecorators'; import { GameCommandBase } from './GameCommandBase'; -import { PapyrusGame } from '../../PapyrusGame'; +import { PapyrusGame } from "../../PapyrusGame"; import { IPathResolver } from '../../common/PathResolver'; import { copyAndFillTemplate, mkDirByPathSync } from '../../Utilities'; diff --git a/src/papyrus-lang-vscode/src/features/commands/InstallDebugSupportCommand.ts b/src/papyrus-lang-vscode/src/features/commands/InstallDebugSupportCommand.ts index 1d5ceea4..c38d7f3c 100644 --- a/src/papyrus-lang-vscode/src/features/commands/InstallDebugSupportCommand.ts +++ b/src/papyrus-lang-vscode/src/features/commands/InstallDebugSupportCommand.ts @@ -1,8 +1,9 @@ import { IDebugSupportInstallService, DebugSupportInstallState } from '../../debugger/DebugSupportInstallService'; import { window, ProgressLocation } from 'vscode'; -import { PapyrusGame, getDisplayNameForGame } from '../../PapyrusGame'; +import { PapyrusGame, getDisplayNameForGame } from "../../PapyrusGame"; import { GameCommandBase } from './GameCommandBase'; -import { getGameIsRunning, waitWhile } from '../../Utilities'; +import { getGameIsRunning } from '../../Utilities'; +import { waitWhile } from "../../VsCodeUtilities"; import { inject, injectable } from 'inversify'; export function showGameDisabledMessage(game: PapyrusGame) { diff --git a/src/papyrus-lang-vscode/src/features/commands/SearchCreationKitWikiCommand.ts b/src/papyrus-lang-vscode/src/features/commands/SearchCreationKitWikiCommand.ts index 66f56adf..9b7f1adf 100644 --- a/src/papyrus-lang-vscode/src/features/commands/SearchCreationKitWikiCommand.ts +++ b/src/papyrus-lang-vscode/src/features/commands/SearchCreationKitWikiCommand.ts @@ -1,7 +1,7 @@ import { EditorCommandBase } from '../../common/vscode/commands/EditorCommandBase'; import { TextEditor, env, Uri, window } from 'vscode'; import { ILanguageClientManager } from '../../server/LanguageClientManager'; -import { PapyrusGame } from '../../PapyrusGame'; +import { PapyrusGame } from "../../PapyrusGame"; import { inject, injectable } from 'inversify'; @injectable() diff --git a/src/papyrus-lang-vscode/src/features/projects/ProjectsTreeDataProvider.ts b/src/papyrus-lang-vscode/src/features/projects/ProjectsTreeDataProvider.ts index 726dcb89..be47519a 100644 --- a/src/papyrus-lang-vscode/src/features/projects/ProjectsTreeDataProvider.ts +++ b/src/papyrus-lang-vscode/src/features/projects/ProjectsTreeDataProvider.ts @@ -10,7 +10,7 @@ import { ProjectInfoSourceInclude, ProjectInfoScript, } from '../../server/messages/ProjectInfos'; -import { PapyrusGame, getShortDisplayNameForGame } from '../../PapyrusGame'; +import { PapyrusGame, getShortDisplayNameForGame } from "../../PapyrusGame"; import { flatten } from '../../Utilities'; import { IExtensionContext } from '../../common/vscode/IocDecorators'; import { inject, injectable } from 'inversify'; diff --git a/src/papyrus-lang-vscode/src/server/LanguageClient.ts b/src/papyrus-lang-vscode/src/server/LanguageClient.ts index 4b9b815d..fd78c3b5 100644 --- a/src/papyrus-lang-vscode/src/server/LanguageClient.ts +++ b/src/papyrus-lang-vscode/src/server/LanguageClient.ts @@ -3,7 +3,7 @@ import { workspace, FileSystemWatcher, OutputChannel } from 'vscode'; import { DocumentScriptInfo, documentScriptInfoRequestType } from './messages/DocumentScriptInfo'; import { DocumentSyntaxTree, documentSyntaxTreeRequestType } from './messages/DocumentSyntaxTree'; -import { PapyrusGame } from '../PapyrusGame'; +import { PapyrusGame } from "../PapyrusGame"; import { toCommandLineArgs } from '../Utilities'; import { ProjectInfos, projectInfosRequestType } from './messages/ProjectInfos'; import { Observable, BehaviorSubject } from 'rxjs'; diff --git a/src/papyrus-lang-vscode/src/server/LanguageClientHost.ts b/src/papyrus-lang-vscode/src/server/LanguageClientHost.ts index 76ca8294..3a03eb1d 100644 --- a/src/papyrus-lang-vscode/src/server/LanguageClientHost.ts +++ b/src/papyrus-lang-vscode/src/server/LanguageClientHost.ts @@ -1,13 +1,13 @@ import { Disposable, OutputChannel, window, TextDocument } from 'vscode'; import { LanguageClient, ILanguageClient, IToolArguments } from './LanguageClient'; -import { PapyrusGame, getShortDisplayNameForGame } from '../PapyrusGame'; +import { PapyrusGame, getShortDisplayNameForGame, getDefaultFlagsFileNameForGame } from "../PapyrusGame"; import { IGameConfig } from '../ExtensionConfigProvider'; import { Observable, BehaviorSubject, of } from 'rxjs'; import { ICreationKitInfo } from '../CreationKitInfoProvider'; import { DocumentScriptInfo } from './messages/DocumentScriptInfo'; -import { shareReplay, take, switchMap } from 'rxjs/operators'; -import { getDefaultFlagsFileNameForGame, IPathResolver } from '../common/PathResolver'; +import { shareReplay, take, map, switchMap } from 'rxjs/operators'; +import { IPathResolver } from '../common/PathResolver'; import { ProjectInfos } from './messages/ProjectInfos'; import { inject } from 'inversify'; @@ -123,12 +123,12 @@ export class LanguageClientHost implements ILanguageClientHost, Disposable { this._status.next(ClientHostStatus.compilerMissing); return; } - + const defaultFlags = getDefaultFlagsFileNameForGame(this._game); const toolArguments: IToolArguments = { compilerAssemblyPath: this._creationKitInfo.resolvedCompilerPath, creationKitInstallPath: this._creationKitInfo.resolvedInstallPath, relativeIniPaths: this._config.creationKitIniFiles, - flagsFileName: getDefaultFlagsFileNameForGame(this._game), + flagsFileName: defaultFlags, ambientProjectName: 'Creation Kit', defaultScriptSourceFolder: this._creationKitInfo.config.Papyrus?.sScriptSourceFolder, defaultAdditionalImports: this._creationKitInfo.config.Papyrus?.sAdditionalImports, diff --git a/src/papyrus-lang-vscode/src/server/LanguageClientManager.ts b/src/papyrus-lang-vscode/src/server/LanguageClientManager.ts index 96536731..41c3c2f3 100644 --- a/src/papyrus-lang-vscode/src/server/LanguageClientManager.ts +++ b/src/papyrus-lang-vscode/src/server/LanguageClientManager.ts @@ -1,6 +1,6 @@ import { inject, injectable, interfaces } from 'inversify'; import { ILanguageClient } from './LanguageClient'; -import { PapyrusGame, getGames } from '../PapyrusGame'; +import { PapyrusGame, getGames } from "../PapyrusGame"; import { Observable, Subscription, combineLatest } from 'rxjs'; import { IExtensionConfigProvider, IGameConfig } from '../ExtensionConfigProvider'; import { map, take } from 'rxjs/operators'; From 4e36917a624403478e797ade8df4ab37c1d270b2 Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Sun, 1 Oct 2023 16:13:58 -0700 Subject: [PATCH 04/15] Add Pex parsing Just using this to get compile times --- .../src/debugger/PexParser.ts | 423 ++++++++++++++++++ 1 file changed, 423 insertions(+) create mode 100644 src/papyrus-lang-vscode/src/debugger/PexParser.ts diff --git a/src/papyrus-lang-vscode/src/debugger/PexParser.ts b/src/papyrus-lang-vscode/src/debugger/PexParser.ts new file mode 100644 index 00000000..a695ade7 --- /dev/null +++ b/src/papyrus-lang-vscode/src/debugger/PexParser.ts @@ -0,0 +1,423 @@ +import { Parser } from 'binary-parser'; +import { readFileSync } from 'fs'; +import { PapyrusGame } from '../PapyrusGame'; + +// This interface contains the same members that are in the "Header" class in F:\workspace\skyrim-mod-workspace\Champollion\Pex\Header.hpp +// The members are in the same order as they are in the C++ header file. +export interface PexHeader { + /** + * Determines the game that the pex file was compiled for. + * This is determined by the endianness of the magic number. + * If the magic number is 0xFA57C0DE, then the game is Fallout 4. + * If the magic number is 0xDEC057FA, then the game is Skyrim. + */ + Game: PapyrusGame; + /** + * The major version game that the pex file was compiled for. + */ + MajorVersion: number; + /** + * The minor version game that the pex file was compiled for. + */ + MinorVersion: number; + /** + * The game ID of the game that the pex file was compiled for. + */ + GameID: number; + /** + * The timestamp of when the pex file was compiled. + */ + CompileTime: number; + /** + * The name of the source file that was compiled to create the pex file. + * This can either be an absolute path (on the *ORIGINAL MACHINE* that it was compiled on) + * or a relative path. + */ + SourceFileName: string; + /** + * The name of the user that compiled the pex file. + * This is the name of the user on the *ORIGINAL MACHINE* that it was compiled on. + */ + UserName: string; + /** + * The name of the computer that compiled the pex file. + */ + ComputerName: string; +} + +export interface PexBinary { + Path: string; + Header: PexHeader; + DebugInfo: DebugInfo; + // TODO: add the rest of the data +} + +export enum FunctionType{ + Method, + Getter, + Setter +} +// names these are all indexed into the string table, but copied into the interface +export interface FunctionInfo { + ObjectName: string; + StateName: string; + FunctionName: string; + FunctionType: FunctionType; + LineNumbers: number[]; +} + +export interface PropertyGroup { + ObjectName: string, + GroupName: string, + DocString: string, + UserFlags: number, + Names: string[] +} + +export interface StructOrder { + StructName: string, + OrderName: string, + Names: string[] +} + +export interface DebugInfo { + ModificationTime: number, + FunctionInfos: FunctionInfo[], + PropertyGroups: PropertyGroup[], + /** + * Fallout 4 only + */ + StructOrders?: StructOrder[] +} + +// TODO: maybe implement this +class PexIndexedString { + public readonly index: number; + public readonly str: string; + constructor(index: number, str: string) { + this.index = index; + this.str = str; + } + + public toString(): string { + return this.str; + } +} + +export class PexStringTable { + public readonly strings: string[]; + constructor(strings: string[]) { + this.strings = strings; + } +} + +const LE_MAGIC_NUMBER = 0xfa57c0de; // 4200055006, values when read little endian +const BE_MAGIC_NUMBER = 0xdec057fa; // 3737147386, values when read little endian + + +function DetermineEndianness(buffer: Buffer) { + const magicNumber = buffer.readUInt32LE(0); + return DetermineEndiannessFromNumber(magicNumber); +} + +function DetermineEndiannessFromNumber(number: number) { + if (number === LE_MAGIC_NUMBER) { + return 'little'; + } else if (number === BE_MAGIC_NUMBER) { + return 'big'; + } else { + return undefined; + } +} + +function getGameFromEndianness(endianness: 'little' | 'big') { + if (endianness === 'little') { + return PapyrusGame.fallout4; + } + return PapyrusGame.skyrimSpecialEdition; +} + +/** + * Parses a pex file. + * + * NOTE: This only currently implements the parsing of the header and the debug info. + */ +export class PexReader { + public readonly path; + + public readonly game: PapyrusGame = PapyrusGame.skyrimSpecialEdition; + constructor(path: string) { + this.path = path; + } + private endianness: "little" | "big" = "little"; + private stringTable: PexStringTable = new PexStringTable([]); + + // constants + + // parsers + private readonly StringParser = () => + new Parser() + .uint16('__strlen') + .string('__string', { length: '__strlen', encoding: 'ascii', zeroTerminated: false }); + + private readonly _strNest = { type: this.StringParser(), formatter: (x): string => x.__string }; + + private readonly StringTableParser = + new Parser().uint16('__tbllen').array('__strings', { + type: this.StringParser(), + formatter: (x): string[] => x.map((y) => y.__string), + length: '__tbllen', + }); + + private readonly _strTableNest = { type: this.StringTableParser , formatter: (x): + PexStringTable => { + // TODO: Global state hack to get around not being able to reference the parsed string table in the middle of the parse + this.stringTable = new PexStringTable(x.__strings); + return this.stringTable; + } + }; + + private readonly FunctionInfoRawParser = () => new Parser() + .uint16('ObjectName') + .uint16('StateName') + .uint16('FunctionName') + .uint8('FunctionType') + .uint16('LineNumbersCount') + .array('LineNumbers', { + type: this.GetUintType(), + length: 'LineNumbersCount', + formatter: (x): number[] => x.map((y) => y.__val) + }) + + + private readonly FunctionInfosParser = () => new Parser().uint16('__FIlen').array('__infos', { + type: this.FunctionInfoRawParser(), + length: '__FIlen', + formatter: (x): FunctionInfo[] => x.map((y) => { + let functinfo = { + ObjectName: this.TableLookup(y.ObjectName), + StateName: this.TableLookup(y.StateName), + FunctionName: this.TableLookup(y.FunctionName), + FunctionType: y.FunctionType as FunctionType, + LineNumbers: y.LineNumbers + } as FunctionInfo; + return functinfo; + }), + }); + public GetEndianness() { + return this.endianness; + } + private GetUintType(){ + return new Parser().uint16("__val"); + } + private readonly PropertyGroupRawParser = () => new Parser() + .uint16('ObjectName') + .uint16('GroupName') + .uint16('DocString') + .uint32('UserFlags') + .uint16('NamesCount') + .array('Names', { + type: this.GetUintType(), + length: 'NamesCount', + formatter: (x): number[] => x.map((y) => y.__val) + }) + private TableLookup (x: number){ + if (x >= this.stringTable.strings.length){ + return ""; + } + return this.stringTable.strings[x]; + } + private readonly PropertyGroupsParser = () => new Parser().uint16('__PGlen').array('__infos', { + type: this.PropertyGroupRawParser(), + length: '__PGlen', + formatter: (x): PropertyGroup[] => x.map((y) => { + let pgroups = { + ObjectName: this.TableLookup(y.ObjectName), + GroupName: this.TableLookup(y.GroupName), + DocString: this.TableLookup(y.DocString), + UserFlags: y.UserFlags, + Names: y.Names.map((z) => this.TableLookup(z)) + } as PropertyGroup; + return pgroups; + }) + }); + + private readonly StructOrderRawParser = () => new Parser() + .uint16('StructName') + .uint16('OrderName') + .uint16('NamesCount') + .array('Names', { + type: this.GetUintType(), + length: 'NamesCount', + formatter: (x): number[] => x.map((y) => y.__val) + + }) + + private readonly StructOrdersParser = () => new Parser().uint16('__SOlen').array('__infos', { + type: this.StructOrderRawParser(), + length: '__SOlen', + formatter: (x): StructOrder[] => x.map((y) => { + let sorders = { + StructName: this.TableLookup(y.StructName), + OrderName: this.TableLookup(y.OrderName), + Names: y.Names.map((z) => this.TableLookup(z)) + } as StructOrder; + return sorders; + }) + }); + + private readonly _doParseDebugInfo = () => { + return new Parser() + .uint64('ModificationTime') + .nest('FunctionInfos', { + type: this.FunctionInfosParser(), + formatter: (x): FunctionInfo[] => x.__infos + }) + .nest('PropertyGroups', { + type: this.PropertyGroupsParser(), + formatter: (x): PropertyGroup[] => x.__infos + }) + .choice("StructOrders", { + tag: () => { + let val =this.endianness === "little" ? 1 : 0 + return val; + }, + choices: { + 0: new Parser().skip(0), + 1: this.StructOrdersParser() + }, + formatter: (x): StructOrder[] | undefined => { + if (this.endianness === "little" && x){ + return x.__infos; + } + return undefined; + } + }) + } + + + private readonly ParseDebugInfo = () => new Parser() + .uint8('HasDebugInfo') + .choice('DebugInfo', { + tag: 'HasDebugInfo', + choices: { + 0: new Parser().skip(0), + 1: this._doParseDebugInfo() + }, + formatter: (x): DebugInfo | undefined => { + if (!x) { + return undefined; + } + return x; + } + }) + + private readonly _debugInfoNest = { type: this.ParseDebugInfo(), formatter: (x): DebugInfo | undefined => x ? x.DebugInfo : undefined }; + + private readonly HeaderParser = () => + new Parser() + .uint32('MagicNumber') + .uint8('MajorVersion') + .uint8('MinorVersion') + .uint16('GameID') + .uint64('CompileTime') + .nest('SourceFileName', this._strNest) + .nest('UserName', this._strNest) + .nest('ComputerName',this. _strNest); + + private readonly _HeaderNest = (endianness: 'little' | 'big') => { + return { + type: this.HeaderParser(), + formatter: (x): PexHeader => { + return { + Game: getGameFromEndianness(endianness), + MajorVersion: x.MajorVersion, + MinorVersion: x.MinorVersion, + GameID: x.GameID, + CompileTime: x.CompileTime, + SourceFileName: x.SourceFileName, + UserName: x.UserName, + ComputerName: x.ComputerName, + }; + }, + }; + }; + + + private ReadPexBinary(buffer: Buffer): PexBinary | undefined { + let endianness: 'little' | 'big' | undefined = DetermineEndianness(buffer); + if (!endianness) { + return undefined; + } + const Pex = new Parser() + .endianess(endianness) + .nest('Header', this._HeaderNest(endianness)) + .nest('StringTable',this._strTableNest) + .nest('DebugInfo', this._debugInfoNest) + .parse(buffer); + + return { + Path: this.path, + Header: Pex.Header, + DebugInfo: Pex.DebugInfo + } + } + + private ReadHeader(buffer: Buffer) { + + return new Parser() + .endianess(this.endianness) + .nest('Header', this._HeaderNest(this.endianness)).parse(buffer); + } + + public async ReadPexHeader(): Promise { + // read the binary file from the path into a byte buffer + //let data = readFileSync(path,{encoding: 'binary'}); + const buffer = readFileSync(this.path); + if (!buffer || buffer.length < 4) { + return undefined; + } + let endianness: 'little' | 'big' | undefined = DetermineEndianness(buffer); + if (!endianness) { + return undefined; + } + this.endianness = endianness; + + return this.ReadHeader(buffer); + } + // not complete + async ReadPex(): Promise{ + const buffer = readFileSync(this.path); + if (!buffer || buffer.length < 4) { + return undefined; + } + let endianness: 'little' | 'big' | undefined = DetermineEndianness(buffer); + if (!endianness) { + return undefined; + } + this.endianness = endianness; + return this.ReadPexBinary(buffer); + } +} + +// returns the 64-bit timestamp of when the pex file was compiled +// if file not found or not parsable, returns -1 +export async function GetCompiledTime(path: string): Promise { + const pex = new PexReader(path); + const header = await pex.ReadPexHeader(); + if (!header) { + return -1; + } + return header.CompileTime; +} + +// // Test the PexReader +// let pexreader = new PexReader( +// 'F:\\workspace\\skyrim-mod-workspace\\papyrus-lang\\src\\papyrus-lang-vscode\\_wetbpautoadjust.pex' +// ); + + +// pexreader.ReadPex().then((pex) => { +// console.log(pex); +// console.log('done'); +// }); From 4d7c8ad56a1dc4e8763c29e41f8ced6fc5538b4d Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Sun, 1 Oct 2023 16:14:10 -0700 Subject: [PATCH 05/15] Add WIP AddressLibInstallService Not hooked up yet, not tested --- .../src/PapyrusExtension.ts | 2 + .../src/common/GithubHelpers.ts | 172 ++++++ .../src/common/PathResolver.ts | 32 +- .../src/common/constants.ts | 28 + .../src/debugger/AddressLibInstallService.ts | 498 ++++++++++++++++++ .../src/debugger/MO2Helpers.ts | 26 +- 6 files changed, 738 insertions(+), 20 deletions(-) create mode 100644 src/papyrus-lang-vscode/src/common/GithubHelpers.ts create mode 100644 src/papyrus-lang-vscode/src/debugger/AddressLibInstallService.ts diff --git a/src/papyrus-lang-vscode/src/PapyrusExtension.ts b/src/papyrus-lang-vscode/src/PapyrusExtension.ts index 4639b0e1..b948d6ed 100644 --- a/src/papyrus-lang-vscode/src/PapyrusExtension.ts +++ b/src/papyrus-lang-vscode/src/PapyrusExtension.ts @@ -27,6 +27,7 @@ import { showWelcome } from './features/WelcomeHandler'; import { ShowWelcomeCommand } from './features/commands/ShowWelcomeCommand'; import { Container } from 'inversify'; import { IDebugLauncherService, DebugLauncherService } from "./debugger/DebugLauncherService"; +import { IAddressLibraryInstallService, AddressLibraryInstallService } from "./debugger/AddressLibInstallService"; class PapyrusExtension implements Disposable { private readonly _serviceContainer: Container; @@ -68,6 +69,7 @@ class PapyrusExtension implements Disposable { this._serviceContainer.bind(ILanguageClientManager).to(LanguageClientManager); this._serviceContainer.bind(IDebugSupportInstallService).to(DebugSupportInstallService); this._serviceContainer.bind(IDebugLauncherService).to(DebugLauncherService); + this._serviceContainer.bind(IAddressLibraryInstallService).to(AddressLibraryInstallService); this._configProvider = this._serviceContainer.get(IExtensionConfigProvider); this._clientManager = this._serviceContainer.get(ILanguageClientManager); diff --git a/src/papyrus-lang-vscode/src/common/GithubHelpers.ts b/src/papyrus-lang-vscode/src/common/GithubHelpers.ts new file mode 100644 index 00000000..24079a7b --- /dev/null +++ b/src/papyrus-lang-vscode/src/common/GithubHelpers.ts @@ -0,0 +1,172 @@ +import { getReleases } from '@terascope/fetch-github-release/dist/src/getReleases'; +import { GithubRelease, GithubReleaseAsset } from '@terascope/fetch-github-release/dist/src/interfaces'; +import { downloadRelease } from '@terascope/fetch-github-release/dist/src/downloadRelease'; +import { getLatest } from '@terascope/fetch-github-release/dist/src/getLatest'; +import * as crypto from 'crypto'; +import * as path from 'path'; +import * as fs from 'fs'; +import { promisify } from 'util'; + +const readdir = promisify(fs.readdir); + +export enum DownloadResult { + success, + repoFailure, + sha256sumDownloadFailure, + filesystemFailure, + downloadFailure, + checksumMismatch, + releaseHasMultipleMatchingAssets, + cancelled, +} + +export async function CheckHash(data: Buffer, expectedHash: string) { + const hash = crypto.createHash('sha256'); + hash.update(data); + const actualHash = hash.digest('hex'); + if (expectedHash !== actualHash) { + return false; + } + return true; +} + +export async function GetHashOfFolder(folderPath: string, inputHash?: crypto.Hash): Promise{ + const hash = inputHash ? inputHash : crypto.createHash('sha256'); + const info = await readdir(folderPath, {withFileTypes: true}); + if (!info || info.length == 0) { + return undefined; + } + for (let item of info) { + const fullPath = path.join(folderPath, item.name); + if (item.isFile()) { + const data = fs.readFileSync(fullPath); + hash.update(data); + } else if (item.isDirectory()) { + // recursively walk sub-folders + await GetHashOfFolder(fullPath, hash); + } + } + return hash.digest('hex'); +} + +export async function CheckHashOfFolder(folderPath: string, expectedSHA256: string): Promise { + const hash = await GetHashOfFolder(folderPath); + if (!hash) { + return false; + } + if (hash !== expectedSHA256){ + return false; + } + return true; +} + +export async function CheckHashFile(filePath: string, expectedSHA256: string) { + // get the hash of the file + const file = fs.openSync(filePath, 'r'); + if (!file) { + return false; + } + const buffer = fs.readFileSync(file); + if (!buffer) { + return false; + } + const hash = crypto.createHash('sha256'); + hash.update(buffer); + const actualHash = hash.digest('hex'); + if (expectedSHA256 !== actualHash) { + return false; + } + return true; +} + +/** + * Downloads all assets from a specific release + * @param githubUserName The name of the user or organization that owns the repo + * @param repoName The name of the repo + * @param releaseId The id of the release + * @param downloadFolder The folder to download the assets to + * @returns An array of paths to the downloaded assets + * @throws An error if the repo does not exist or the release does not exist + * @throws An error if the release has multiple assets with the same name + * @throws An error if the download fails +*/ +export async function downloadAssetsFromGitHub(githubUserName: string, repoName: string, releaseId: number, downloadFolder: string): Promise { + const paths = await downloadRelease(githubUserName, repoName, downloadFolder, (release) => release.id == releaseId, undefined, true); + if (!paths || paths.length == 0){ + return undefined; + } + return paths; +} + +/** + * Downloads a specific asset from a specific release + * @param githubUserName The name of the user or organization that owns the repo + * @param repoName The name of the repo + * @param release_id The id of the release + * @param assetFileName The file name of the asset to download + * @param downloadFolder The folder to download the asset to + * @returns The path to the downloaded asset + * @throws An error if the repo does not exist or the release does not exist + * @throws An error if the release has multiple assets with the same name + * @throws An error if the download fails + * @throws An error if the asset does not exist in the release + */ +export async function downloadAssetFromGitHub(githubUserName: string, repoName: string, release_id: number, assetFileName: string, downloadFolder: string): Promise{ + const paths = await downloadRelease(githubUserName, repoName, downloadFolder, (release) => release.id == release_id, (asset) => asset.name === assetFileName, true); + return (paths && paths.length > 0) ? paths[0] : undefined; +} + +/** + * Downloads a specific asset from a specific release and checks the hash + * @param githubUserName The name of the user or organization that owns the repo + * @param repoName The name of the repo + * @param release_id The id of the release + * @param assetFileName The file name of the asset to download + * @param downloadFolder The folder to download the asset to + * @param expectedSha256Sum The expected SHA256 hash of the file + * @returns status of the download + */ +export async function DownloadAssetAndCheckHash( + githubUserName: string, + RepoName: string, + release_id: number, + assetFileName: string, + downloadFolder: string, + expectedSha256Sum: string +): Promise { + let dlPath: string; + try { + dlPath = await downloadAssetFromGitHub(githubUserName, RepoName, release_id, assetFileName, downloadFolder); + } catch (e) { + return DownloadResult.downloadFailure; + } + if (!dlPath) { + return DownloadResult.downloadFailure; + } + // get the hash of the file + const file = fs.openSync(dlPath, 'r'); + if (!file) { + return DownloadResult.downloadFailure; + } + // get the SHA256 hash of the file using the 'crypto' module + const buffer = fs.readFileSync(file); + if (!buffer || buffer.length == 0) { + return DownloadResult.downloadFailure; + } + // get the hash of the file + if (!CheckHash(buffer, expectedSha256Sum)) { + fs.rmSync(dlPath); + return DownloadResult.checksumMismatch; + } + return DownloadResult.success; +} + +export async function GetLatestReleaseFromRepo(githubUserName: string, repoName: string, prerelease: boolean = false): Promise { + // if pre-releases == false, filter out pre-releases + const releaseFilter = !prerelease ? (release: GithubRelease) => release.prerelease == false : undefined; + const latestRelease = await getLatest(await getReleases(githubUserName, repoName), releaseFilter); + if (!latestRelease) { + return undefined; + } + return latestRelease; +} \ No newline at end of file diff --git a/src/papyrus-lang-vscode/src/common/PathResolver.ts b/src/papyrus-lang-vscode/src/common/PathResolver.ts index ab6d1b2e..a12e81b7 100644 --- a/src/papyrus-lang-vscode/src/common/PathResolver.ts +++ b/src/papyrus-lang-vscode/src/common/PathResolver.ts @@ -19,6 +19,8 @@ const exists = promisify(fs.exists); export interface IPathResolver { // Internal paths getDebugPluginBundledPath(game: PapyrusGame): Promise; + getAddressLibraryDownloadFolder(): Promise; + getAddressLibraryDownloadJSON(): Promise; getLanguageToolPath(game: PapyrusGame): Promise; getDebugToolPath(game: PapyrusGame): Promise; getPyroCliPath(): Promise; @@ -28,6 +30,7 @@ export interface IPathResolver { // External paths getInstallPath(game: PapyrusGame): Promise; getModDirectoryPath(game: PapyrusGame): Promise; + getModParentPath(game: PapyrusGame): Promise; getDebugPluginInstallPath(game: PapyrusGame, legacy?: boolean): Promise; } @@ -75,6 +78,14 @@ export class PathResolver implements IPathResolver { return this._asExtensionAbsolutePath(path.join(bundledPluginPath, getPluginDllName(game))); } + public async getAddressLibraryDownloadFolder() { + return this._asExtensionAbsolutePath(downloadedAddressLibraryPath); + } + + public async getAddressLibraryDownloadJSON() { + return this._asExtensionAbsolutePath(path.join(downloadedAddressLibraryPath, "address-library.json")); + } + public async getLanguageToolPath(game: PapyrusGame): Promise { const toolGameName = getToolGameName(game); return this._asExtensionAbsolutePath( @@ -145,7 +156,25 @@ export class PathResolver implements IPathResolver { return config.modDirectoryPath; } - dispose() {} + + /** + * If the mod directory is set, then this just returns the mod directory + * Otherwise, it returns "${game directory}/Data" + * @param game + * @returns + */ + public async getModParentPath(game: PapyrusGame): Promise { + const modDirectoryPath = await this.getModDirectoryPath(game); + if (modDirectoryPath) { + return modDirectoryPath; + } + const installPath = await this.getInstallPath(game); + if (!installPath) { + return null; + } + return path.join(installPath, "Data"); + } + dispose() { } } export const IPathResolver: interfaces.ServiceIdentifier = Symbol('pathResolver'); @@ -155,6 +184,7 @@ export const IPathResolver: interfaces.ServiceIdentifier = Symbol /************************************************************************* */ const bundledPluginPath = 'debug-plugin'; +const downloadedAddressLibraryPath = 'debug-address-library'; function getPluginDllName(game: PapyrusGame, legacy = false) { switch (game) { diff --git a/src/papyrus-lang-vscode/src/common/constants.ts b/src/papyrus-lang-vscode/src/common/constants.ts index 3c010cdc..fab81d77 100644 --- a/src/papyrus-lang-vscode/src/common/constants.ts +++ b/src/papyrus-lang-vscode/src/common/constants.ts @@ -12,3 +12,31 @@ export const AddressLibraryF4SEModName = "Address Library for F4SE Plugins"; export const AddressLibrarySKSEAEModName = "Address Library for SKSE Plugins (AE)"; export const AddressLibrarySKSEModName = "Address Library for SKSE Plugins"; +// TODO: Move these elsewhere +export type AddressLibraryName = typeof AddressLibraryF4SEModName | typeof AddressLibrarySKSEAEModName | typeof AddressLibrarySKSEModName; +export enum AddressLibAssetSuffix { + SkyrimSE = 'SkyrimSE', + SkyrimAE = 'SkyrimAE', + Fallout4 = 'Fallout4', +} + +export function getAsssetLibraryDLSuffix(addlibname: AddressLibraryName): AddressLibAssetSuffix { + switch (addlibname) { + case AddressLibrarySKSEModName: + return AddressLibAssetSuffix.SkyrimSE; + case AddressLibrarySKSEAEModName: + return AddressLibAssetSuffix.SkyrimAE; + case AddressLibraryF4SEModName: + return AddressLibAssetSuffix.Fallout4 + } +} +export function getAddressLibNameFromAssetSuffix(suffix: AddressLibAssetSuffix): AddressLibraryName { + switch (suffix) { + case AddressLibAssetSuffix.SkyrimSE: + return AddressLibrarySKSEModName; + case AddressLibAssetSuffix.SkyrimAE: + return AddressLibrarySKSEAEModName; + case AddressLibAssetSuffix.Fallout4: + return AddressLibraryF4SEModName; + } +} diff --git a/src/papyrus-lang-vscode/src/debugger/AddressLibInstallService.ts b/src/papyrus-lang-vscode/src/debugger/AddressLibInstallService.ts new file mode 100644 index 00000000..a3e8c091 --- /dev/null +++ b/src/papyrus-lang-vscode/src/debugger/AddressLibInstallService.ts @@ -0,0 +1,498 @@ +import { inject, injectable, interfaces } from 'inversify'; +import { IExtensionConfigProvider } from '../ExtensionConfigProvider'; +import { CancellationToken, CancellationTokenSource } from 'vscode'; +import { IPathResolver } from '../common/PathResolver'; +import { PapyrusGame } from '../PapyrusGame'; +import { ILanguageClientManager } from '../server/LanguageClientManager'; + +import { mkdirIfNeeded } from '../Utilities'; + +import { GithubRelease } from '@terascope/fetch-github-release/dist/src/interfaces'; + +import * as path from 'path'; +import * as fs from 'fs'; +import { promisify } from 'util'; + +import md5File from 'md5-file'; +import { + AddressLibraryName, + AddressLibAssetSuffix, + getAddressLibNameFromAssetSuffix, + getAsssetLibraryDLSuffix, +} from '../common/constants'; +import extractZip from 'extract-zip'; +import { + CheckHashFile, + DownloadAssetAndCheckHash, + downloadAssetFromGitHub, + DownloadResult, + GetHashOfFolder, + GetLatestReleaseFromRepo, +} from '../common/GithubHelpers'; +import { getAddressLibNames } from './MO2Helpers'; +const exists = promisify(fs.exists); +const copyFile = promisify(fs.copyFile); +const removeFile = promisify(fs.unlink); + +export enum AddressLibDownloadedState { + notDownloaded, + latest, + outdated, + downloadedButCantCheckForUpdates, +} + +export enum AddressLibInstalledState { + notInstalled, + installed, + outdated, + installedButCantCheckForUpdates +} + +export interface Asset { + /** + * The file name of the zip file + */ + zipFile: string; + folderName: AddressLibraryName; + zipFileHash: string; + /** + * For checking if the installed folder has the same folder hash as the one we have + */ + folderHash: string; +} +export interface AddressLibReleaseAssetList { + version: string; + // The name of the zip file + SkyrimSE: Asset; + // The name of the zip file + SkyrimAE: Asset; + // The name of the zip file + Fallout4: Asset; +} + +const AddLibRepoUserName = 'nikitalita'; +const AddLibRepoName = 'address-library-dist'; + +function _getAsset(assetList: AddressLibReleaseAssetList, suffix: AddressLibAssetSuffix): Asset | undefined { + return assetList[suffix]; +} + +export interface IAddressLibraryInstallService { + getInstallState(game: PapyrusGame, modsDir?: string): Promise; + getDownloadedState(): Promise; + DownloadLatestAddressLibs(cancellationToken: CancellationToken): Promise; + installLibraries( + game: PapyrusGame, + forceDownload: boolean, + cancellationToken?: CancellationToken, + modsDir?: string + ): Promise; +} + +@injectable() +export class AddressLibraryInstallService implements IAddressLibraryInstallService { + private readonly _pathResolver: IPathResolver; + + constructor( + @inject(ILanguageClientManager) languageClientManager: ILanguageClientManager, + @inject(IExtensionConfigProvider) configProvider: IExtensionConfigProvider, + @inject(IPathResolver) pathResolver: IPathResolver + ) { + this._pathResolver = pathResolver; + } + + private static _getAssetZipForSuffixFromRelease( + release: GithubRelease, + name: AddressLibAssetSuffix + ): string | undefined { + const _assets = release.assets.filter((asset) => asset.name.indexOf(name) >= 0); + if (_assets.length == 0) { + return undefined; + } else if (_assets.length > 1) { + // This should never happen + throw new Error('Too many assets found for suffix: ' + name + ''); + } + return _assets[0].name; + } + + private static getAssetListFromAddLibRelease(release: GithubRelease): AddressLibReleaseAssetList | undefined { + let assetZip: string | undefined | Error; + let ret: AddressLibReleaseAssetList; + ret.version = release.tag_name; + for (let _assetSuffix in AddressLibAssetSuffix) { + const assetSuffix = _assetSuffix as AddressLibAssetSuffix; + + assetZip = AddressLibraryInstallService._getAssetZipForSuffixFromRelease( + release, + assetSuffix + ); + if (!assetZip) { + return undefined; + } + ret[assetSuffix] = { + zipFile: assetZip, + folderName: getAddressLibNameFromAssetSuffix(assetSuffix), + zipFileHash: "0", + folderHash: "0", + }; + } + return ret; + } + + private static async getLatestAddLibReleaseInfo(): Promise { + let latestReleaseInfo: GithubRelease | undefined; + try { + latestReleaseInfo = await GetLatestReleaseFromRepo(AddLibRepoUserName, AddLibRepoName, false); + if (!latestReleaseInfo) { + return undefined; + } + } catch (e) { + return undefined; + } + return latestReleaseInfo; + } + + private static async _downloadLatestAddressLibs( + downloadFolder: string, + AssetListDLPath: string, + cancellationToken: CancellationToken + ) { + const latestReleaseInfo = await AddressLibraryInstallService.getLatestAddLibReleaseInfo(); + if (!latestReleaseInfo) { + return DownloadResult.repoFailure; + } + const assetList = AddressLibraryInstallService.getAssetListFromAddLibRelease(latestReleaseInfo); + if (!assetList) { + return DownloadResult.repoFailure; + } + const release_id = latestReleaseInfo.id; + // get the shasums + let sha256SumsPath: string; + try { + sha256SumsPath = await downloadAssetFromGitHub( + AddLibRepoUserName, + AddLibRepoName, + release_id, + 'SHA256SUMS.json', + downloadFolder + ); + if (!sha256SumsPath) { + return DownloadResult.sha256sumDownloadFailure; + } + } catch (e) { + return DownloadResult.sha256sumDownloadFailure; + } + + const sha256buf = fs.readFileSync(sha256SumsPath, 'utf8'); + if (!sha256buf) { + return DownloadResult.sha256sumDownloadFailure; + } + const sha256Sums = JSON.parse(sha256buf); + const retryLimit = 3; + let retries = 0; + for (const _assetSuffix in AddressLibAssetSuffix) { + const assetSuffix = _assetSuffix as AddressLibAssetSuffix; + if (cancellationToken.isCancellationRequested) { + return DownloadResult.cancelled; + } + retries = 0; + const asset = _getAsset(assetList, assetSuffix); + if (!asset) { + return DownloadResult.repoFailure; + } + const expectedHash = sha256Sums[asset.zipFile]; + if (!expectedHash) { + return DownloadResult.repoFailure; + } + let ret: DownloadResult = await DownloadAssetAndCheckHash( + AddLibRepoName, + AddLibRepoUserName, + release_id, + asset.zipFile, + downloadFolder, + expectedHash + ); + + while (retries < retryLimit && cancellationToken.isCancellationRequested == false) { + ret = await DownloadAssetAndCheckHash( + AddLibRepoName, + AddLibRepoUserName, + release_id, + asset.zipFile, + downloadFolder, + expectedHash + ); + if (ret == DownloadResult.success) { + break; + } + retries++; + } + if (cancellationToken.isCancellationRequested) { + return DownloadResult.cancelled; + } + if (ret != DownloadResult.success) { + return ret; + } + asset.zipFileHash = expectedHash; + const zipFilePath = path.join(downloadFolder, asset.zipFile); + // We extract the zip here to check the hash of the folder when we check the install state + // We don't end up installing from the folder, we install from the zip + const ExtractedFolderPath = path.join(downloadFolder, asset.folderName); + fs.rmSync(ExtractedFolderPath, { recursive: true, force: true }); + await extractZip(zipFilePath, { dir: ExtractedFolderPath }); + if (!await AddressLibraryInstallService._checkAddlibExtracted(asset.folderName, ExtractedFolderPath)) { + return DownloadResult.filesystemFailure; + } + asset.folderHash = await GetHashOfFolder(ExtractedFolderPath); + if (asset.folderHash === undefined) { + return DownloadResult.filesystemFailure; + } + // Remove it, because we don't install from it + fs.rmSync(ExtractedFolderPath, { recursive: true, force: true }); + assetList[assetSuffix] = asset; + } + // we do this last to make sure we don't write a corrupt json file + fs.writeFileSync(AssetListDLPath, JSON.stringify(assetList)); + return DownloadResult.success; + } + + async DownloadLatestAddressLibs(cancellationToken = new CancellationTokenSource().token): Promise { + const addressLibDownloadPath = await this._pathResolver.getAddressLibraryDownloadFolder(); + const addressLibDLJSONPath = await this._pathResolver.getAddressLibraryDownloadJSON(); + let status = await AddressLibraryInstallService._downloadLatestAddressLibs( + addressLibDownloadPath, + addressLibDLJSONPath, + cancellationToken + ); + return status; + } + + private static async _getAssetList(jsonPath: string) { + if (!fs.existsSync(jsonPath)) { + return undefined; + } + const contents = fs.readFileSync(jsonPath, 'utf8'); + if (!contents || contents.length == 0) { + // json is corrupt + return undefined; + } + const assetList = JSON.parse(contents) as AddressLibReleaseAssetList; + if (!assetList) { + // json is corrupt + return undefined; + } + // check integrity + for (const _assetSuffix in AddressLibAssetSuffix) { + const assetSuffix = _assetSuffix as AddressLibAssetSuffix; + const currentAsset = _getAsset(assetList, assetSuffix); + if (!currentAsset) { + // json is corrupt + return undefined; + } + if ( + !currentAsset.zipFile || + !currentAsset.zipFileHash || + !currentAsset.folderName || + !currentAsset.folderHash + ) { + // json is corrupt + return undefined; + } + } + return assetList; + } + + private async getCurrentDownloadedAssetList(): Promise { + const addressLibDLJSONPath = await this._pathResolver.getAddressLibraryDownloadJSON(); + return AddressLibraryInstallService._getAssetList(addressLibDLJSONPath); + } + + private static async _checkDownloadIntegrity( + downloadpath: string, + assetList: AddressLibReleaseAssetList + ): Promise { + if (!assetList) { + return false; + } + for (const _assetSuffix in AddressLibAssetSuffix) { + const assetSuffix = _assetSuffix as AddressLibAssetSuffix; + const currentAsset = _getAsset(assetList, assetSuffix); + const assetName = currentAsset.zipFile; + const assetPath = path.join(downloadpath, assetName); + if (!fs.existsSync(assetPath)) { + return false; + } + if (!CheckHashFile(assetPath, currentAsset.zipFileHash)) { + return false; + } + } + return true; + } + + private async checkDownloadIntegrity(): Promise { + const addressLibDownloadPath = await this._pathResolver.getAddressLibraryDownloadFolder(); + const assetList = await this.getCurrentDownloadedAssetList(); + if (!assetList) { + return false; + } + return await AddressLibraryInstallService._checkDownloadIntegrity(addressLibDownloadPath, assetList); + } + + /** + * Gets the state of the address library download + * - If the address library is not downloaded or the download is corrupt, it will return `notDownloaded` + * - If the address library is downloaded but the version can't be checked, it will return `downloadedButCantCheck` + * - If the address library is downloaded but the version is outdated, it will return `outdated` + * - If the address library is downloaded and the version is up to date, it will return `latest` + * @returns AddressLibDownloadedState + */ + async getDownloadedState(): Promise { + // If it's not downloaded or the download is corrupt, we return notDownloaded + if (!(await this.checkDownloadIntegrity())) { + return AddressLibDownloadedState.notDownloaded; + } + // At this point, we know if SOME version is downloaded and is valid, but we don't know if it's the latest + const assetList = await this.getCurrentDownloadedAssetList(); + if (!assetList) { + return AddressLibDownloadedState.notDownloaded; + } + const latestReleaseInfo = await AddressLibraryInstallService.getLatestAddLibReleaseInfo(); + if (!latestReleaseInfo) { + return AddressLibDownloadedState.downloadedButCantCheckForUpdates; + } + const latestAssetList = AddressLibraryInstallService.getAssetListFromAddLibRelease(latestReleaseInfo); + if (!latestAssetList) { + return AddressLibDownloadedState.downloadedButCantCheckForUpdates; + } + if (latestAssetList.version != assetList.version) { + return AddressLibDownloadedState.outdated; + } + return AddressLibDownloadedState.latest; + } + + /** + * @param game The game to check for + * @param modsDir The mods directory to check in + * @param assetList The downloaded Address Library asset list to check against. + * If not provided, we don't check if it's outdated + * @returns AddressLibInstalledState + */ + private static async _checkAddressLibsInstalled( + game: PapyrusGame, + modsDir: string, + assetList?: AddressLibReleaseAssetList + ): Promise { + const addressLibFolderNames = getAddressLibNames(game); + for (let _name in addressLibFolderNames) { + const name = _name as AddressLibraryName; + const suffix = getAsssetLibraryDLSuffix(name); + const addressLibInstallPath = path.join(modsDir, name); + if (!await AddressLibraryInstallService._checkAddlibExtracted(name, addressLibInstallPath)) { + return AddressLibInstalledState.notInstalled; + } + if (assetList) { + const asset = _getAsset(assetList, suffix); + if (!asset) { + throw new Error('Asset list is corrupt'); + } + const folderHash = await GetHashOfFolder(addressLibInstallPath); + if (!folderHash) { + return AddressLibInstalledState.notInstalled; + } + if (folderHash != asset.folderHash) { + return AddressLibInstalledState.outdated; + } + } + } + return AddressLibInstalledState.installed; + } + + async getInstallState(game: PapyrusGame, modsDir?: string): Promise { + const ModsInstallDir = modsDir || (await this._pathResolver.getModParentPath(game)); + const state = await AddressLibraryInstallService._checkAddressLibsInstalled(game, modsDir); + if (state === AddressLibInstalledState.notInstalled) { + return AddressLibInstalledState.notInstalled; + } + + // At this point, we know if the address libraries are installed or not, but we don't know if they're outdated + const downloadedState = await this.getDownloadedState(); + // We don't check the installed address lib versions if we don't have the latest version downloaded + if (downloadedState !== AddressLibDownloadedState.latest) { + return AddressLibInstalledState.installedButCantCheckForUpdates; + } + const assetList = await this.getCurrentDownloadedAssetList(); + if (!assetList) { + return AddressLibInstalledState.installedButCantCheckForUpdates; + } + return await AddressLibraryInstallService._checkAddressLibsInstalled(game, ModsInstallDir, assetList); + } + /** + * This checks to see if the folder has at least one file in it + * @param name + * @param installpath - the full path to the folder to check, including the address library name + * @returns + */ + private static async _checkAddlibExtracted(name: AddressLibraryName, installpath: string): Promise { + if (!fs.existsSync(installpath)) { + return false; + } + // TODO: refactor this + const SEDIR = name.indexOf('SKSE') >= 0 ? 'SKSE' : 'F4SE'; + const pluginsdir = path.join(installpath, SEDIR, 'Plugins'); + const files = fs.readdirSync(pluginsdir, {withFileTypes: true }); + if (files.length == 0) { + return false; + } + return true; + } + + async installLibraries( + game: PapyrusGame, + forceDownload: boolean = false, + cancellationToken = new CancellationTokenSource().token, + modsDir: string | undefined + ): Promise { + const ParentInstallDir = modsDir || (await this._pathResolver.getModParentPath(game)); + const addressLibDownloadPath = await this._pathResolver.getAddressLibraryDownloadFolder(); + let downloadedState = await this.getDownloadedState(); + if (downloadedState === AddressLibDownloadedState.notDownloaded) { + if (forceDownload) { + if ((await this.DownloadLatestAddressLibs(cancellationToken)) != DownloadResult.success) { + return false; + } + } else { + return false; + } + } + + const assetList = await this.getCurrentDownloadedAssetList(); + + if (!assetList) { + return false; + } + const addressLibNames = getAddressLibNames(game); + for (let _name in addressLibNames) { + const name = _name as AddressLibraryName; + if (cancellationToken.isCancellationRequested) { + return false; + } + const suffix = getAsssetLibraryDLSuffix(name); + const asset = _getAsset(assetList, suffix); + if (!asset) { + throw new Error('Asset list is corrupt'); + } + const addressLibInstallPath = path.join(ParentInstallDir, name); + await mkdirIfNeeded(path.dirname(addressLibInstallPath)); + await extractZip(path.join(addressLibDownloadPath, asset.zipFile), { + dir: path.dirname(addressLibInstallPath), + }); + if (!await AddressLibraryInstallService._checkAddlibExtracted(name, addressLibInstallPath)) { + return false; + } + } + return true; + } +} + +export const IAddressLibraryInstallService: interfaces.ServiceIdentifier = + Symbol('AddressLibraryInstallService'); diff --git a/src/papyrus-lang-vscode/src/debugger/MO2Helpers.ts b/src/papyrus-lang-vscode/src/debugger/MO2Helpers.ts index 7b2352af..58202c29 100644 --- a/src/papyrus-lang-vscode/src/debugger/MO2Helpers.ts +++ b/src/papyrus-lang-vscode/src/debugger/MO2Helpers.ts @@ -8,6 +8,7 @@ import * as ini from 'ini'; import { AddressLibraryF4SEModName, + AddressLibraryName, AddressLibrarySKSEAEModName, AddressLibrarySKSEModName, PDSModName, @@ -33,8 +34,6 @@ export class ModListItem { -// path helpers -// TODO: Move these to pathresolver /** * Will return the path to the plugins folder for the given game relative to the game's data folder * @@ -60,7 +59,7 @@ export function GetGlobalGameSavedDataFolder(game: PapyrusGame){ if (process.env.HOMEPATH === undefined) { return undefined; } - return path.join(process.env.HOMEPATH, "My Games", getRegistryKeyForGame(game)); + return path.join(process.env.HOMEPATH, "Documents", "My Games", getRegistryKeyForGame(game)); } export function GetLocalAppDataFolder(){ @@ -100,7 +99,6 @@ export async function FindMO2InstanceIniPath(modsFolder: string, MO2EXEPath: str } export async function FindMO2ProfilesFolder(modsFolder: string, MO2InstanceIniData : INIData): Promise { - let parentFolder = path.dirname(modsFolder); let profilesFolder = path.join(parentFolder, 'profiles'); if (existsSync(profilesFolder)) { @@ -124,26 +122,16 @@ export async function FindMO2ProfilesFolder(modsFolder: string, MO2InstanceIniDa } return undefined; } -function getAddressLibNames(game: PapyrusGame) { + +export function getAddressLibNames(game: PapyrusGame): AddressLibraryName[] { if (game === PapyrusGame.fallout4) { return [AddressLibraryF4SEModName]; + } else if (game === PapyrusGame.skyrimSpecialEdition) { + return [AddressLibrarySKSEModName, AddressLibrarySKSEAEModName]; } - return [AddressLibrarySKSEModName, AddressLibrarySKSEAEModName]; + throw new Error("ERROR: Unsupported game!"); } -// export async function FindMO2ProfileFolder(profileName: string, modFolder: string, MO2InstanceIniData : INIData ): Promise { -// let profilesFolder = await FindMO2ProfilesFolder(modFolder, MO2InstanceIniData); -// if (profilesFolder === undefined) { -// return undefined; -// } -// let profileFolder = path.join(profilesFolder, profileName); - -// if (existsSync(profileFolder)) { -// return profileFolder; -// } -// return undefined; -// } - export async function ParseIniFile(IniPath: string): Promise { let IniText = readFileSync(IniPath, 'utf-8'); if (!IniText) { From 5f34542a771886e4ec3bcacf590837739f95134f Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Sun, 1 Oct 2023 16:14:36 -0700 Subject: [PATCH 06/15] Search for GOG and Epic versions of Skyrim --- src/papyrus-lang-vscode/src/PapyrusGame.ts | 40 ++++ .../src/common/GameHelpers.ts | 200 ++++++++++++++++++ .../src/common/OSHelpers.ts | 26 +++ .../src/common/PathResolver.ts | 75 ++++--- 4 files changed, 311 insertions(+), 30 deletions(-) create mode 100644 src/papyrus-lang-vscode/src/common/GameHelpers.ts create mode 100644 src/papyrus-lang-vscode/src/common/OSHelpers.ts diff --git a/src/papyrus-lang-vscode/src/PapyrusGame.ts b/src/papyrus-lang-vscode/src/PapyrusGame.ts index 6b275a72..ad4cfa63 100644 --- a/src/papyrus-lang-vscode/src/PapyrusGame.ts +++ b/src/papyrus-lang-vscode/src/PapyrusGame.ts @@ -27,7 +27,14 @@ const scriptExtenderNames = new Map([ export function getScriptExtenderName(game: PapyrusGame) { return scriptExtenderNames.get(game); } +const scriptExtenderExecutableNames = new Map([ + [PapyrusGame.fallout4, 'f4se_loader.exe'], + [PapyrusGame.skyrimSpecialEdition, 'skse64_loader.exe'], +]); +export function getScriptExtenderExecutableName(game: PapyrusGame) { + return scriptExtenderExecutableNames.get(game); +} const scriptExtenderUrls = new Map([ [PapyrusGame.fallout4, 'https://f4se.silverlock.org/'], [PapyrusGame.skyrimSpecialEdition, 'https://skse.silverlock.org/'], @@ -80,3 +87,36 @@ const executableNames = new Map([ export function getExecutableNameForGame(game: PapyrusGame) { return executableNames.get(game)!; } + +export function getGameIniName(game: PapyrusGame): string { + return game == PapyrusGame.fallout4 ? 'fallout4.ini' :'skyrim.ini'; +} + +export enum GameVariant { + Steam = "Steam", + GOG = "GOG", + Epic = "Epic Games" +} + +/** + * returns the name of the Game Save folder for the given variant + * @param variant + * @returns + */ +export function GetUserGameFolderName(game: PapyrusGame, variant: GameVariant){ + switch (game) { + case PapyrusGame.fallout4: + return "Fallout4"; + case PapyrusGame.skyrim: + return "Skyrim"; + case PapyrusGame.skyrimSpecialEdition: + switch (variant) { + case GameVariant.Steam: + return "Skyrim Special Edition"; + case GameVariant.GOG: + return "Skyrim Special Edition GOG"; + case GameVariant.Epic: + return "Skyrim Special Edition EPIC"; + } + } +} diff --git a/src/papyrus-lang-vscode/src/common/GameHelpers.ts b/src/papyrus-lang-vscode/src/common/GameHelpers.ts new file mode 100644 index 00000000..b02c241c --- /dev/null +++ b/src/papyrus-lang-vscode/src/common/GameHelpers.ts @@ -0,0 +1,200 @@ +import path from "path"; +import { getRegistryKeyForGame, PapyrusGame, GameVariant, GetUserGameFolderName, getScriptExtenderName } from "../PapyrusGame"; +import * as fs from "fs"; +import { promisify } from "util"; +import { AddressLibAssetSuffix, AddressLibraryF4SEModName, AddressLibraryName, AddressLibrarySKSEAEModName, AddressLibrarySKSEModName } from "./constants"; +import { INIData } from "./INIHelpers"; +import { getHomeFolder, getRegistryValueData } from "./OSHelpers"; + +const exists = promisify(fs.exists); +const readdir = promisify(fs.readdir); +const readFile = promisify(fs.readFile); + +export function getAsssetLibraryDLSuffix(addlibname: AddressLibraryName): AddressLibAssetSuffix { + switch (addlibname) { + case AddressLibrarySKSEModName: + return AddressLibAssetSuffix.SkyrimSE; + case AddressLibrarySKSEAEModName: + return AddressLibAssetSuffix.SkyrimAE; + case AddressLibraryF4SEModName: + return AddressLibAssetSuffix.Fallout4 + } +} + +export function getAddressLibNameFromAssetSuffix(suffix: AddressLibAssetSuffix): AddressLibraryName { + switch (suffix) { + case AddressLibAssetSuffix.SkyrimSE: + return AddressLibrarySKSEModName; + case AddressLibAssetSuffix.SkyrimAE: + return AddressLibrarySKSEAEModName; + case AddressLibAssetSuffix.Fallout4: + return AddressLibraryF4SEModName; + } +} + +export function getAddressLibNames(game: PapyrusGame): AddressLibraryName[] { + if (game === PapyrusGame.fallout4) { + return [AddressLibraryF4SEModName]; + } else if (game === PapyrusGame.skyrimSpecialEdition) { + return [AddressLibrarySKSEModName, AddressLibrarySKSEAEModName]; + } + // there is no skyrim classic address library + return []; +} + +export function CheckIfDebuggingIsEnabledInIni(iniData: INIData) { + return ( + iniData.Papyrus.bLoadDebugInformation === 1 && + iniData.Papyrus.bEnableTrace === 1 && + iniData.Papyrus.bEnableLogging === 1 + ); +} + +export function TurnOnDebuggingInIni(skyrimIni: INIData) { + const _ini = structuredClone(skyrimIni); + _ini.Papyrus.bLoadDebugInformation = 1; + _ini.Papyrus.bEnableTrace = 1; + _ini.Papyrus.bEnableLogging = 1; + return _ini; +} + +export async function FindUserGamePath(game: PapyrusGame, variant: GameVariant): Promise { + let GameFolderName: string = GetUserGameFolderName(game, variant); + let home = getHomeFolder(); + if (!home) { + return null; + } + let userGamePath = path.join(home, 'Documents', 'My Games', GameFolderName); + if (await exists(userGamePath)) { + return userGamePath; + } + return null; +} + +/** + * We need to determine variants for things like the save game path + * @param game + * @param installPath + * @returns + */ +export async function DetermineGameVariant(game: PapyrusGame, installPath: string): Promise { + // only Skyrim SE has variants, the rest are only sold on steam + if (game !== PapyrusGame.skyrimSpecialEdition){ + return GameVariant.Steam; + } + if (!installPath || !(await exists(installPath))) { + // just default to steam + return GameVariant.Steam + } + const gog_dll = path.join(installPath, 'Galaxy64.dll'); + const epic_dll = path.join(installPath, 'EOSSDK-Win64-Shipping.dll'); + if (await exists(gog_dll)) { + return GameVariant.GOG; + } + if (await exists(epic_dll)) { + return GameVariant.Epic; + } + // default to steam + return GameVariant.Steam; +} + +async function findSkyrimSEEpic(): Promise { + const key = `\\SOFTWARE\\${process.arch === 'x64' ? 'WOW6432Node\\' : ''}Epic Games\\EpicGamesLauncher`; + const val = 'AppDataPath'; + let epicAppdatapath = await getRegistryValueData(key, val); + let manifestsDir: string; + if (epicAppdatapath) { + manifestsDir = path.join(epicAppdatapath, 'Manifests'); + } else if (process.env.PROGRAMDATA) { + // if the local app data path isn't set, try the global one + manifestsDir = path.join(process.env.PROGRAMDATA, 'Epic', 'EpicGamesLauncher', 'Data', 'Manifests'); + } else { + return null; + } + if (await exists(manifestsDir)) { + // list the directory and find the manifest for Skyrim SE + const manifestFiles = await readdir(manifestsDir); + for (const manifestFile of manifestFiles) { + // read the manifest file and check if it's for Skyrim SE + if (path.extname(manifestFile) !== '.item') { + continue; + } + let data = await readFile(path.join(manifestsDir, manifestFile), 'utf8'); + if (data) { + let manifest = JSON.parse(data); + if ( + manifest && + manifest.AppName && + (manifest.AppName === 'ac82db5035584c7f8a2c548d98c86b2c' || + manifest.AppName === '5d600e4f59974aeba0259c7734134e27') + ) { + if (manifest.InstallLocation && (await exists(manifest.InstallLocation))) { + return manifest.InstallLocation; + } + } + } + } + } + return null; +} + +async function findSkyrimSEGOG(): Promise { + const keynames = [ + // check Skyrim AE first + `\\SOFTWARE\\${process.arch === 'x64' ? 'WOW6432Node\\' : ''}GOG.com\\Games\\1162721350`, + // If AE isn't installed, check Skyrim SE + `\\SOFTWARE\\${process.arch === 'x64' ? 'WOW6432Node\\' : ''}GOG.com\\Games\\1711230643`, + ]; + for (const key of keynames) { + let gogpath = await getRegistryValueData(key, 'path'); + if (gogpath && (await exists(gogpath))) { + return gogpath; + } + } + return null; +} + +async function FindGameSteamPath(game: PapyrusGame): Promise { + const key = `\\SOFTWARE\\${ + process.arch === 'x64' ? 'WOW6432Node\\' : '' + }Bethesda Softworks\\${getRegistryKeyForGame(game)}`; + const val = 'installed path'; + const pathValue = await getRegistryValueData(key, val); + if (pathValue && (await exists(pathValue))) { + return pathValue; + } + return null; +} + +export async function FindGamePath(game: PapyrusGame) +{ + if (game === PapyrusGame.fallout4 || game === PapyrusGame.skyrim) { + return FindGameSteamPath(game); + } else if (game === PapyrusGame.skyrimSpecialEdition) { + let path = await FindGameSteamPath(game); + if (path) { + return path; + } + path = await findSkyrimSEGOG(); + if (path) { + return path; + } + path = await findSkyrimSEEpic(); + if (path) { + return path; + } + } + return null; + +} + +/** + * Will return the path to the plugins folder for the given game relative to the game's data folder + * + * + * @param game fallout4 or skyrimse + * @returns + */ +export function getRelativePluginPath(game: PapyrusGame) { + return `${getScriptExtenderName(game)}/Plugins`; +} diff --git a/src/papyrus-lang-vscode/src/common/OSHelpers.ts b/src/papyrus-lang-vscode/src/common/OSHelpers.ts new file mode 100644 index 00000000..cee77d2f --- /dev/null +++ b/src/papyrus-lang-vscode/src/common/OSHelpers.ts @@ -0,0 +1,26 @@ +import { promisify } from 'util'; +import winreg from 'winreg'; + +export function getLocalAppDataFolder(){ + return process.env.LOCALAPPDATA; +} +export function getHomeFolder() { + return process.env.HOMEPATH; +} +export function getUserName(){ + return process.env.USERNAME; +} +export function getTempFolder(){ + return process.env.TEMP; +} +export async function getRegistryValueData(key: string, value: string, hive: string = 'HKLM') { + let reg = new winreg({ + hive, + key, + }); + try { + const item = await promisify(reg.get).call(reg, value); + return item.value; + } catch (e) {} + return null; +} diff --git a/src/papyrus-lang-vscode/src/common/PathResolver.ts b/src/papyrus-lang-vscode/src/common/PathResolver.ts index a12e81b7..d47571c7 100644 --- a/src/papyrus-lang-vscode/src/common/PathResolver.ts +++ b/src/papyrus-lang-vscode/src/common/PathResolver.ts @@ -3,18 +3,20 @@ import * as path from 'path'; import { inject, injectable, interfaces } from 'inversify'; import { take } from 'rxjs/operators'; -import winreg from 'winreg'; import { promisify } from 'util'; import { ExtensionContext } from 'vscode'; import { IExtensionContext } from '../common/vscode/IocDecorators'; -import { PapyrusGame, getScriptExtenderName, getRegistryKeyForGame } from "../PapyrusGame"; +import { PapyrusGame, getScriptExtenderName } from '../PapyrusGame'; import { inDevelopmentEnvironment } from '../Utilities'; -import { IExtensionConfigProvider, IGameConfig, IExtensionConfig } from '../ExtensionConfigProvider'; +import { IExtensionConfigProvider, IGameConfig } from '../ExtensionConfigProvider'; import { PDSModName } from './constants'; +import { DetermineGameVariant, FindGamePath, FindUserGamePath } from './GameHelpers'; const exists = promisify(fs.exists); +const readdir = promisify(fs.readdir); +const readFile = promisify(fs.readFile); export interface IPathResolver { // Internal paths @@ -29,6 +31,7 @@ export interface IPathResolver { getWelcomeFile(): Promise; // External paths getInstallPath(game: PapyrusGame): Promise; + getUserGamePath(game: PapyrusGame): Promise; getModDirectoryPath(game: PapyrusGame): Promise; getModParentPath(game: PapyrusGame): Promise; getDebugPluginInstallPath(game: PapyrusGame, legacy?: boolean): Promise; @@ -75,7 +78,11 @@ export class PathResolver implements IPathResolver { /************************************************************************* */ public async getDebugPluginBundledPath(game: PapyrusGame) { - return this._asExtensionAbsolutePath(path.join(bundledPluginPath, getPluginDllName(game))); + let dll = getPluginDllName(game); + if (!dll){ + throw new Error("Debugging not supported for game " + game); + } + return this._asExtensionAbsolutePath(path.join(bundledPluginPath, dll)); } public async getAddressLibraryDownloadFolder() { @@ -83,7 +90,7 @@ export class PathResolver implements IPathResolver { } public async getAddressLibraryDownloadJSON() { - return this._asExtensionAbsolutePath(path.join(downloadedAddressLibraryPath, "address-library.json")); + return this._asExtensionAbsolutePath(path.join(downloadedAddressLibraryPath, addlibManifestName)); } public async getLanguageToolPath(game: PapyrusGame): Promise { @@ -127,13 +134,19 @@ export class PathResolver implements IPathResolver { return resolveInstallPath(game, config.installPath, this._context); } + public async getUserGamePath(game: PapyrusGame): Promise { + const config = await this._getGameConfig(game); + return resolveUserGamePath(game, config.installPath, this._context); + } + // TODO: Refactor this properly. public async getDebugPluginInstallPath(game: PapyrusGame, legacy?: boolean): Promise { const modDirectoryPath = await this.getModDirectoryPath(game); if (modDirectoryPath) { return path.join( - modDirectoryPath, PDSModName, + modDirectoryPath, + PDSModName, PathResolver._getModMgrExtenderPluginRelativePath(game), getPluginDllName(game, legacy) ); @@ -156,12 +169,11 @@ export class PathResolver implements IPathResolver { return config.modDirectoryPath; } - /** * If the mod directory is set, then this just returns the mod directory * Otherwise, it returns "${game directory}/Data" - * @param game - * @returns + * @param game + * @returns */ public async getModParentPath(game: PapyrusGame): Promise { const modDirectoryPath = await this.getModDirectoryPath(game); @@ -172,9 +184,9 @@ export class PathResolver implements IPathResolver { if (!installPath) { return null; } - return path.join(installPath, "Data"); + return path.join(installPath, 'Data'); } - dispose() { } + dispose() {} } export const IPathResolver: interfaces.ServiceIdentifier = Symbol('pathResolver'); @@ -185,15 +197,15 @@ export const IPathResolver: interfaces.ServiceIdentifier = Symbol const bundledPluginPath = 'debug-plugin'; const downloadedAddressLibraryPath = 'debug-address-library'; - -function getPluginDllName(game: PapyrusGame, legacy = false) { +const addlibManifestName = 'address-library.json'; +export function getPluginDllName(game: PapyrusGame, legacy = false) { switch (game) { case PapyrusGame.fallout4: return legacy ? 'DarkId.Papyrus.DebugServer.dll' : 'DarkId.Papyrus.DebugServer.Fallout4.dll'; case PapyrusGame.skyrimSpecialEdition: return 'DarkId.Papyrus.DebugServer.Skyrim.dll'; default: - throw new Error(`'${game}' is not supported by the Papyrus debugger.`); + throw new Error("Debugging not supported for game " + game); } } @@ -219,23 +231,12 @@ export async function resolveInstallPath( if (await exists(installPath)) { return installPath; } - const regkey = getRegistryKeyForGame( - game - ); - const reg = new winreg({ - key: `\\SOFTWARE\\${process.arch === 'x64' ? 'WOW6432Node\\' : ''}Bethesda Softworks\\${regkey}`, - }); - - try { - const item = await promisify(reg.get).call(reg, 'installed path'); - - if (await exists(item.value)) { - return item.value; - } - } catch (_) { - // empty on purpose + const pathValue = await FindGamePath(game); + if (pathValue) { + return pathValue; } + // TODO: @joelday, what is this for? if (inDevelopmentEnvironment() && game !== PapyrusGame.skyrim) { return context.asAbsolutePath('../../dependencies/compilers'); } @@ -243,7 +244,21 @@ export async function resolveInstallPath( return null; } - +async function resolveUserGamePath( + game: PapyrusGame, + installPath: string, + context: ExtensionContext +): Promise { + let _installPath : string | null = installPath; + if (!(await exists(installPath))) { + _installPath = await resolveInstallPath(game, installPath, context); + } + if (!installPath) { + return null; + } + const variant = await DetermineGameVariant(game, installPath); + return FindUserGamePath(game, variant); +} export function pathToOsPath(pathName: string) { return path.format(path.parse(pathName)); From d84c190a50d62c16276a09706d55d7803e0e0107 Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Sun, 1 Oct 2023 16:17:29 -0700 Subject: [PATCH 07/15] working automatic launch configuration --- .vscode/launch.json | 44 +- .vscode/tasks.json | 66 +- build.cake | 16 +- src/papyrus-lang-vscode/.gitignore | 1 + src/papyrus-lang-vscode/package-lock.json | 12 + src/papyrus-lang-vscode/package.json | 47 +- .../src/PapyrusExtension.ts | 4 + src/papyrus-lang-vscode/src/PapyrusGame.ts | 1 + src/papyrus-lang-vscode/src/Utilities.ts | 129 ++- .../src/common/GithubHelpers.ts | 80 +- .../src/common/INIHelpers.ts | 67 ++ src/papyrus-lang-vscode/src/common/MO2Lib.ts | 982 ++++++++++++++++++ .../src/common/constants.ts | 21 - .../src/debugger/AddLibHelpers.ts | 383 +++++++ .../src/debugger/AddressLibInstallService.ts | 411 +------- .../src/debugger/DebugLauncherService.ts | 58 +- .../debugger/DebugSupportInstallService.ts | 50 +- .../debugger/GameDebugConfiguratorService.ts | 103 ++ .../src/debugger/MO2ConfiguratorService.ts | 301 ++++++ .../src/debugger/MO2Helpers.ts | 467 ++++----- .../debugger/MO2LaunchDescriptorFactory.ts | 504 +++------ .../PapyrusDebugAdapterDescriptorFactory.ts | 171 ++- .../PapyrusDebugConfigurationProvider.ts | 188 ++-- .../src/debugger/PapyrusDebugSession.ts | 102 +- .../src/debugger/PexParser.ts | 45 +- .../commands/InstallDebugSupportCommand.ts | 45 +- src/papyrus-lang-vscode/tsconfig.json | 10 +- 27 files changed, 2912 insertions(+), 1396 deletions(-) create mode 100644 src/papyrus-lang-vscode/src/common/INIHelpers.ts create mode 100644 src/papyrus-lang-vscode/src/common/MO2Lib.ts create mode 100644 src/papyrus-lang-vscode/src/debugger/AddLibHelpers.ts create mode 100644 src/papyrus-lang-vscode/src/debugger/GameDebugConfiguratorService.ts create mode 100644 src/papyrus-lang-vscode/src/debugger/MO2ConfiguratorService.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index 2fa18be7..613c990f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -13,7 +13,26 @@ "args": [ "--extensionDevelopmentPath=${workspaceFolder}/src/papyrus-lang-vscode" ], - "outFiles": ["${workspaceFolder}/src/papyrus-lang-vscode/out/**/*.js"] + "outFiles": [ + "${workspaceFolder}/src/papyrus-lang-vscode/dist/*.js" + ], + }, + { + "name": "Launch (Build extension and build and copy binaries only)", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "preLaunchTask": "buildExtensionAndUpdateBin", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}/src/papyrus-lang-vscode" + ], + "sourceMaps": true, + "outFiles": [ + "${workspaceFolder}/src/papyrus-lang-vscode/dist/*.js" + ], + "cwd": "${workspaceFolder}/src/papyrus-lang-vscode", + "internalConsoleOptions": "openOnSessionStart", + "outputCapture": "console" }, { "name": "Launch (Build and copy binaries only)", @@ -24,18 +43,29 @@ "args": [ "--extensionDevelopmentPath=${workspaceFolder}/src/papyrus-lang-vscode" ], - "outFiles": ["${workspaceFolder}/src/papyrus-lang-vscode/out/**/*.js"] + "sourceMaps": true, + "outFiles": [ + "${workspaceFolder}/src/papyrus-lang-vscode/dist/*.js" + ], + "cwd": "${workspaceFolder}/src/papyrus-lang-vscode", + "internalConsoleOptions": "openOnSessionStart", + "outputCapture": "console" }, { "name": "Launch (Build extension only)", "type": "extensionHost", "request": "launch", + "pauseForSourceMap": true, "runtimeExecutable": "${execPath}", "preLaunchTask": "buildExtension", "args": [ "--extensionDevelopmentPath=${workspaceFolder}/src/papyrus-lang-vscode" ], - "outFiles": ["${workspaceFolder}/src/papyrus-lang-vscode/out/**/*.js"] + "outFiles": [ + "${workspaceFolder}/src/papyrus-lang-vscode/dist/*.js" + ], + "sourceMaps": true, + "internalConsoleOptions": "openOnSessionStart" }, { "name": "Launch (No build)", @@ -45,7 +75,11 @@ "args": [ "--extensionDevelopmentPath=${workspaceFolder}/src/papyrus-lang-vscode" ], - "outFiles": ["${workspaceFolder}/src/papyrus-lang-vscode/out/**/*.js"] + "outFiles": [ + "${workspaceFolder}/src/papyrus-lang-vscode/dist/*.js" + ], + "sourceMaps": true, + "internalConsoleOptions": "openOnSessionStart" } ] -} +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 545a5cd4..4f86bbe0 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -15,7 +15,13 @@ "cake" ] }, - "problemMatcher": ["$tsc", "$msCompile"] + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [ + "$tsc", + "$msCompile" + ] }, { "label": "updateBin", @@ -26,9 +32,38 @@ "--target=\"update-bin\"" ] }, + "options": { + "cwd": "${workspaceFolder}" + }, + "command": "./build.sh", + "args": [ + "--target=\"update-bin\"" + ], + "problemMatcher": [ + "$tsc", + "$msCompile" + ] + }, + { + "label": "buildExtensionAndUpdateBin", + "windows": { + "command": "dotnet", + "args": [ + "cake", + "--target=\"build-extension-and-update-bin\"" + ] + }, + "options": { + "cwd": "${workspaceFolder}" + }, "command": "./build.sh", - "args": ["--target=\"update-bin\""], - "problemMatcher": ["$tsc", "$msCompile"] + "args": [ + "--target=\"build-extension-and-update-bin\"" + ], + "problemMatcher": [ + "$tsc", + "$msCompile" + ] }, { "label": "buildExtension", @@ -39,9 +74,28 @@ "--target=\"build-extension\"" ] }, + "options": { + "cwd": "${workspaceFolder}" + }, "command": "./build.sh", - "args": ["--target=\"build-extension\""], - "problemMatcher": ["$tsc", "$msCompile"] + "args": [ + "--target=\"build-extension\"" + ], + "problemMatcher": [ + "$tsc", + "$msCompile" + ] + }, + { + "label": "tsc", + "type": "shell", + "command": "tsc", + "options": { + "cwd": "${workspaceFolder}/src/papyrus-lang-vscode" + }, + "problemMatcher": [ + "$tsc" + ] } ] -} +} \ No newline at end of file diff --git a/build.cake b/build.cake index 8fba1e30..611c258a 100644 --- a/build.cake +++ b/build.cake @@ -210,14 +210,17 @@ Task("copy-debug-plugin") CreateDirectory("./src/papyrus-lang-vscode/debug-plugin"); var configuration = isRelease ? "Release" : "Debug"; + var skyrimPath = $"src/DarkId.Papyrus.DebugServer/bin/DarkId.Papyrus.DebugServer.Skyrim/x64/{configuration}/DarkId.Papyrus.DebugServer.Skyrim.dll"; + var fallout4Path = $"src/DarkId.Papyrus.DebugServer/bin/DarkId.Papyrus.DebugServer.Fallout4/x64/{configuration}/DarkId.Papyrus.DebugServer.Fallout4.dll"; + var copyDir = "./src/papyrus-lang-vscode/debug-plugin"; CopyFileToDirectory( - $"src/DarkId.Papyrus.DebugServer/bin/DarkId.Papyrus.DebugServer.Skyrim/x64/{configuration}/DarkId.Papyrus.DebugServer.Skyrim.dll", - "./src/papyrus-lang-vscode/debug-plugin"); + skyrimPath, + copyDir); CopyFileToDirectory( - $"src/DarkId.Papyrus.DebugServer/bin/DarkId.Papyrus.DebugServer.Fallout4/x64/{configuration}/DarkId.Papyrus.DebugServer.Fallout4.dll", - "./src/papyrus-lang-vscode/debug-plugin"); + fallout4Path, + copyDir); } catch (Exception) { @@ -353,6 +356,11 @@ Task("update-bin") Task("build-extension") .IsDependentOn("npm-build"); +Task("build-extension-and-update-bin") + .IsDependentOn("build-debugger") + .IsDependentOn("update-bin") + .IsDependentOn("build-extension"); + Task("build-test") .IsDependentOn("build") .IsDependentOn("test"); diff --git a/src/papyrus-lang-vscode/.gitignore b/src/papyrus-lang-vscode/.gitignore index d92c2618..64652688 100644 --- a/src/papyrus-lang-vscode/.gitignore +++ b/src/papyrus-lang-vscode/.gitignore @@ -1,5 +1,6 @@ debug-bin debug-plugin +debug-address-library pyro out node_modules diff --git a/src/papyrus-lang-vscode/package-lock.json b/src/papyrus-lang-vscode/package-lock.json index 208e238c..5b9b3459 100644 --- a/src/papyrus-lang-vscode/package-lock.json +++ b/src/papyrus-lang-vscode/package-lock.json @@ -9,6 +9,7 @@ "version": "3.0.0", "dependencies": { "@semantic-release/exec": "^6.0.3", + "@tybys/windows-file-version-info": "^1.0.5", "@types/semantic-release": "^17.2.4", "deepmerge": "^4.2.2", "fast-deep-equal": "^3.1.3", @@ -776,6 +777,12 @@ "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", "dev": true }, + "node_modules/@tybys/windows-file-version-info": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@tybys/windows-file-version-info/-/windows-file-version-info-1.0.5.tgz", + "integrity": "sha512-a5s4m8fFCf/bp+KcawwPTxk1ptftmWWAvsIxorI/K92DgXcCtqIvhW3z7WzXMl4E0yep+WoHTfIz4tnJldjnhg==", + "hasInstallScript": true + }, "node_modules/@types/eslint": { "version": "8.4.10", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.10.tgz", @@ -10197,6 +10204,11 @@ "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", "dev": true }, + "@tybys/windows-file-version-info": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@tybys/windows-file-version-info/-/windows-file-version-info-1.0.5.tgz", + "integrity": "sha512-a5s4m8fFCf/bp+KcawwPTxk1ptftmWWAvsIxorI/K92DgXcCtqIvhW3z7WzXMl4E0yep+WoHTfIz4tnJldjnhg==" + }, "@types/eslint": { "version": "8.4.10", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.10.tgz", diff --git a/src/papyrus-lang-vscode/package.json b/src/papyrus-lang-vscode/package.json index feadf977..8a2821ec 100644 --- a/src/papyrus-lang-vscode/package.json +++ b/src/papyrus-lang-vscode/package.json @@ -67,38 +67,41 @@ "type": "string", "enum": [ "XSE", - "mo2" + "MO2" ] }, - "XSELoaderPath": { + "launcherPath": { "type": "string" }, "mo2Config": { "type": "object", "properties": { - "mo2Path": { + "shortcutURI": { "type": "string" }, - "shortcut": { + "profile": { "type": "string" }, - "modsFolder": { - "type": "string" - }, - "args": { + "instanceINIPath": { "type": "array" } }, "required": [ - "mo2Path", - "shortcut", - "modsFolder" - ] + "shortcutURI" + ], + "additionalProperties": false + }, + "args": { + "type": "array" + }, + "ignoreConfigChecks": { + "type": "boolean" } }, "required": [ "game", - "launchType" + "launchType", + "launcherPath" ] } }, @@ -110,7 +113,7 @@ "type": "papyrus", "game": "${3:fallout4}", "request": "attach", - "projectPath": "^\"\\${workspaceFolder}/${1:Project.ppj}\"" + "projectPath": "^\"\\${workspaceFolder}/${1:fallout4.ppj}\"" } }, { @@ -120,9 +123,9 @@ "type": "papyrus", "game": "${3:fallout4}", "request": "launch", - "projectPath": "^\"\\${workspaceFolder}/${1:Project.ppj}\"", - "launchType": "skse", - "XSELoaderPath": "^\"${5:C:/Program Files (x86)/Steam/steamapps/common/Fallout 4/F4SE/F4SE_loader.exe}\"" + "projectPath": "^\"\\${workspaceFolder}/${1:fallout4.ppj}\"", + "launchType": "XSE", + "launcherPath": "^\"${5:C:/Program Files (x86)/Steam/steamapps/common/Fallout 4/F4SE/F4SE_loader.exe}\"" } }, { @@ -132,12 +135,11 @@ "type": "papyrus", "game": "${3:fallout4}", "request": "launch", - "launchType": "mo2", - "projectPath": "^\"\\${workspaceFolder}/${1:Project.ppj}\"", + "launchType": "MO2", + "projectPath": "^\"\\${workspaceFolder}/${1:fallout4.ppj}\"", + "launcherPath": "^\"${6:C:/Modding/MO2/ModOrganizer.exe}\"", "mo2Config": { - "mo2Path": "^\"${6:C:/Modding/MO2/ModOrganizer.exe}\"", - "shortcut": "^\"${7:moshortcut://Skyrim Special Edition:SKSE}\"", - "modsFolder": "^\"${8:\\${env:LOCALAPPDATA\\}/ModOrganizer/Fallout 4/mods}\"" + "shortcutURI": "^\"${7:moshortcut://Skyrim Special Edition:SKSE}\"" } } } @@ -547,6 +549,7 @@ ], "dependencies": { "@semantic-release/exec": "^6.0.3", + "@tybys/windows-file-version-info": "^1.0.5", "@types/semantic-release": "^17.2.4", "deepmerge": "^4.2.2", "fast-deep-equal": "^3.1.3", diff --git a/src/papyrus-lang-vscode/src/PapyrusExtension.ts b/src/papyrus-lang-vscode/src/PapyrusExtension.ts index b948d6ed..5b6cf4a5 100644 --- a/src/papyrus-lang-vscode/src/PapyrusExtension.ts +++ b/src/papyrus-lang-vscode/src/PapyrusExtension.ts @@ -28,6 +28,8 @@ import { ShowWelcomeCommand } from './features/commands/ShowWelcomeCommand'; import { Container } from 'inversify'; import { IDebugLauncherService, DebugLauncherService } from "./debugger/DebugLauncherService"; import { IAddressLibraryInstallService, AddressLibraryInstallService } from "./debugger/AddressLibInstallService"; +import { IMO2LaunchDescriptorFactory, MO2LaunchDescriptorFactory } from "./debugger/MO2LaunchDescriptorFactory"; +import { IMO2ConfiguratorService, MO2ConfiguratorService } from "./debugger/MO2ConfiguratorService"; class PapyrusExtension implements Disposable { private readonly _serviceContainer: Container; @@ -70,6 +72,8 @@ class PapyrusExtension implements Disposable { this._serviceContainer.bind(IDebugSupportInstallService).to(DebugSupportInstallService); this._serviceContainer.bind(IDebugLauncherService).to(DebugLauncherService); this._serviceContainer.bind(IAddressLibraryInstallService).to(AddressLibraryInstallService); + this._serviceContainer.bind(IMO2LaunchDescriptorFactory).to(MO2LaunchDescriptorFactory); + this._serviceContainer.bind(IMO2ConfiguratorService).to(MO2ConfiguratorService); this._configProvider = this._serviceContainer.get(IExtensionConfigProvider); this._clientManager = this._serviceContainer.get(ILanguageClientManager); diff --git a/src/papyrus-lang-vscode/src/PapyrusGame.ts b/src/papyrus-lang-vscode/src/PapyrusGame.ts index ad4cfa63..198fc527 100644 --- a/src/papyrus-lang-vscode/src/PapyrusGame.ts +++ b/src/papyrus-lang-vscode/src/PapyrusGame.ts @@ -92,6 +92,7 @@ export function getGameIniName(game: PapyrusGame): string { return game == PapyrusGame.fallout4 ? 'fallout4.ini' :'skyrim.ini'; } +// TODO: Support VR export enum GameVariant { Steam = "Steam", GOG = "GOG", diff --git a/src/papyrus-lang-vscode/src/Utilities.ts b/src/papyrus-lang-vscode/src/Utilities.ts index 9472b973..ec68e3bf 100644 --- a/src/papyrus-lang-vscode/src/Utilities.ts +++ b/src/papyrus-lang-vscode/src/Utilities.ts @@ -1,5 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; +import * as crypto from 'crypto'; import { promisify } from 'util'; import procList from 'ps-list'; @@ -8,11 +9,16 @@ import { getExecutableNameForGame, PapyrusGame } from "./PapyrusGame"; import { isNativeError } from 'util/types'; -import { getSystemErrorMap } from 'util'; - +import { + getSystemErrorMap +} from "util"; +import { execFile as _execFile } from 'child_process'; +const execFile = promisify(_execFile); const readFile = promisify(fs.readFile); const writeFile = promisify(fs.writeFile); const exists = promisify(fs.exists); +const readdir = promisify(fs.readdir); +const stat = promisify(fs.stat); export function* flatten(arrs: T[][]): IterableIterator { for (const arr of arrs) { @@ -43,6 +49,37 @@ export async function getGamePIDs(game: PapyrusGame): Promise> { return gameProcesses.map((p) => p.pid); } +export async function getPIDforProcessName(processName: string): Promise> { + const processList = await procList(); + let thing = processList[0]; + const gameProcesses = processList.filter((p) => p.name.toLowerCase() === processName.toLowerCase()); + + if (gameProcesses.length === 0) { + return []; + } + + return gameProcesses.map((p) => p.pid); +} +export async function getPathFromProcess(pid: number){ + let pwsh_cmd = `(Get-Process -id ${pid}).Path` + + var {stdout, stderr} = await execFile('powershell', [ + pwsh_cmd + ]) + if (stderr){ + return undefined; + } + return stdout; +} + +export async function getPIDsforFullPath(processPath: string): Promise> { + const pidsList = await getPIDforProcessName(path.basename(processPath)); + let pids = pidsList.filter(async (pid) => { + return processPath === await getPathFromProcess(pid); + }); + return pids; +} + export function inDevelopmentEnvironment() { return process.execArgv.some((arg) => arg.startsWith('--inspect-brk')); } @@ -128,3 +165,91 @@ export async function copyAndFillTemplate(srcPath: string, dstPath: string, valu } return writeFile(dstPath, templStr); } + +export interface EnvData{ + [key: string]: string; +} + + +export async function getEnvFromProcess(pid: number){ + let pwsh_cmd = `(Get-Process -id ${pid}).StartInfo.EnvironmentVariables.ForEach( { $_.Key + "=" + $_.Value } )` + + var {stdout, stderr} = await execFile('powershell', [ + pwsh_cmd + ]) + if (stderr){ + return undefined; + } + let otherEnv: EnvData = {} + stdout.split('\r\n').forEach((line) => { + let [key, value] = line.split('='); + if (key && key !== ''){ + otherEnv[key] = value; + } + }); + return otherEnv; +} + +export async function CheckHash(data: Buffer, expectedHash: string) { + const hash = crypto.createHash('sha256'); + hash.update(data); + const actualHash = hash.digest('hex'); + if (expectedHash !== actualHash) { + return false; + } + return true; +} + +async function _GetHashOfFolder(folderPath: string, inputHash?: crypto.Hash): Promise{ + if (!inputHash) { + return undefined; + } + const info = await readdir(folderPath, {withFileTypes: true}); + if (!info || info.length == 0) { + return undefined; + } + for (let item of info) { + const fullPath = path.join(folderPath, item.name); + if (item.isFile()) { + const data = fs.readFileSync(fullPath); + inputHash.update(data); + } else if (item.isDirectory()) { + // recursively walk sub-folders + await _GetHashOfFolder(fullPath, inputHash); + } + } + return inputHash; +} + +export async function GetHashOfFolder(folderPath: string): Promise{ + return (await _GetHashOfFolder(folderPath, crypto.createHash('sha256')))?.digest('hex'); +} + +export async function CheckHashOfFolder(folderPath: string, expectedSHA256: string): Promise { + const hash = await GetHashOfFolder(folderPath); + if (!hash) { + return false; + } + if (hash !== expectedSHA256){ + return false; + } + return true; +} + +export async function CheckHashFile(filePath: string, expectedSHA256: string) { + // get the hash of the file + if (!await exists(filePath) || !(await stat(filePath)).isFile()) { + return false; + } + const buffer = await readFile(filePath); + if (!buffer) { + return false; + } + const hash = crypto.createHash('sha256'); + hash.update(buffer); + const actualHash = hash.digest('hex'); + if (expectedSHA256 !== actualHash) { + return false; + } + return true; +} diff --git a/src/papyrus-lang-vscode/src/common/GithubHelpers.ts b/src/papyrus-lang-vscode/src/common/GithubHelpers.ts index 24079a7b..d48d8f24 100644 --- a/src/papyrus-lang-vscode/src/common/GithubHelpers.ts +++ b/src/papyrus-lang-vscode/src/common/GithubHelpers.ts @@ -2,13 +2,12 @@ import { getReleases } from '@terascope/fetch-github-release/dist/src/getRelease import { GithubRelease, GithubReleaseAsset } from '@terascope/fetch-github-release/dist/src/interfaces'; import { downloadRelease } from '@terascope/fetch-github-release/dist/src/downloadRelease'; import { getLatest } from '@terascope/fetch-github-release/dist/src/getLatest'; -import * as crypto from 'crypto'; -import * as path from 'path'; import * as fs from 'fs'; import { promisify } from 'util'; +import { CheckHashFile } from '../Utilities'; const readdir = promisify(fs.readdir); - +const exists = promisify(fs.exists); export enum DownloadResult { success, repoFailure, @@ -20,64 +19,6 @@ export enum DownloadResult { cancelled, } -export async function CheckHash(data: Buffer, expectedHash: string) { - const hash = crypto.createHash('sha256'); - hash.update(data); - const actualHash = hash.digest('hex'); - if (expectedHash !== actualHash) { - return false; - } - return true; -} - -export async function GetHashOfFolder(folderPath: string, inputHash?: crypto.Hash): Promise{ - const hash = inputHash ? inputHash : crypto.createHash('sha256'); - const info = await readdir(folderPath, {withFileTypes: true}); - if (!info || info.length == 0) { - return undefined; - } - for (let item of info) { - const fullPath = path.join(folderPath, item.name); - if (item.isFile()) { - const data = fs.readFileSync(fullPath); - hash.update(data); - } else if (item.isDirectory()) { - // recursively walk sub-folders - await GetHashOfFolder(fullPath, hash); - } - } - return hash.digest('hex'); -} - -export async function CheckHashOfFolder(folderPath: string, expectedSHA256: string): Promise { - const hash = await GetHashOfFolder(folderPath); - if (!hash) { - return false; - } - if (hash !== expectedSHA256){ - return false; - } - return true; -} - -export async function CheckHashFile(filePath: string, expectedSHA256: string) { - // get the hash of the file - const file = fs.openSync(filePath, 'r'); - if (!file) { - return false; - } - const buffer = fs.readFileSync(file); - if (!buffer) { - return false; - } - const hash = crypto.createHash('sha256'); - hash.update(buffer); - const actualHash = hash.digest('hex'); - if (expectedSHA256 !== actualHash) { - return false; - } - return true; -} /** * Downloads all assets from a specific release @@ -134,27 +75,16 @@ export async function DownloadAssetAndCheckHash( downloadFolder: string, expectedSha256Sum: string ): Promise { - let dlPath: string; + let dlPath: string | undefined; try { dlPath = await downloadAssetFromGitHub(githubUserName, RepoName, release_id, assetFileName, downloadFolder); } catch (e) { return DownloadResult.downloadFailure; } - if (!dlPath) { - return DownloadResult.downloadFailure; - } - // get the hash of the file - const file = fs.openSync(dlPath, 'r'); - if (!file) { - return DownloadResult.downloadFailure; - } - // get the SHA256 hash of the file using the 'crypto' module - const buffer = fs.readFileSync(file); - if (!buffer || buffer.length == 0) { + if (!dlPath || !await exists(dlPath)) { return DownloadResult.downloadFailure; } - // get the hash of the file - if (!CheckHash(buffer, expectedSha256Sum)) { + if (!CheckHashFile(dlPath, expectedSha256Sum)) { fs.rmSync(dlPath); return DownloadResult.checksumMismatch; } diff --git a/src/papyrus-lang-vscode/src/common/INIHelpers.ts b/src/papyrus-lang-vscode/src/common/INIHelpers.ts new file mode 100644 index 00000000..7317f7a6 --- /dev/null +++ b/src/papyrus-lang-vscode/src/common/INIHelpers.ts @@ -0,0 +1,67 @@ +import * as ini from 'ini'; +import * as fs from "fs"; +import { promisify } from 'util'; +const readFile = promisify(fs.readFile); + + +export interface INIData { + [key: string]: any; +} + +export function ParseIniArray(data: INIData): INIData[] | undefined { + if (!data || data.size === undefined || data.size === null) { + return undefined; + } + let array = new Array(); + if (data.size === 0) { + return array; + } + for (let i = 0; i < data.size; i++) { + array.push({} as INIData); + } + // Keys in INI arrays are in the format of 1\{key1}, 1\{key2}, 2\{key1}, 2\{key2}, etc. + let keys = Object.keys(data); + keys.forEach((key) => { + if (key !== 'size') { + const parts = key.split('\\'); + if (parts.length === 2) { + // INI arrays are 1-indexed + const index = parseInt(parts[0], 10) - 1; + const subKey = parts[1]; + array[index][subKey] = data[key]; + } + } + }); + return array; +} + +export function SerializeIniArray(data: INIData[]): INIData { + let iniData = {} as INIData; + iniData.size = data.length; + data.forEach((value, index) => { + Object.keys(value).forEach((key) => { + iniData[`${index + 1}\\${key}`] = value[key]; + }); + }); + return iniData; +} + +export async function ParseIniFile(IniPath: string): Promise { + if (!fs.existsSync(IniPath) || !fs.lstatSync(IniPath).isFile()) { + return undefined; + } + let IniText = await readFile(IniPath, 'utf-8'); + if (!IniText) { + return undefined; + } + return ini.parse(IniText) as INIData; +} + +export async function WriteChangesToIni(gameIniPath: string, skyrimIni: INIData) { + const file = fs.openSync(gameIniPath, 'w'); + if (!file) { + return false; + } + fs.writeFileSync(file, ini.stringify(skyrimIni)); + return false; +} diff --git a/src/papyrus-lang-vscode/src/common/MO2Lib.ts b/src/papyrus-lang-vscode/src/common/MO2Lib.ts new file mode 100644 index 00000000..0e9883c5 --- /dev/null +++ b/src/papyrus-lang-vscode/src/common/MO2Lib.ts @@ -0,0 +1,982 @@ +import { existsSync, openSync, readdirSync, readFileSync, writeFileSync } from 'fs'; +import * as fs from 'fs'; +import path from 'path'; +import { INIData, ParseIniArray, ParseIniFile, SerializeIniArray, WriteChangesToIni } from './INIHelpers'; +import { getLocalAppDataFolder, getRegistryValueData } from './OSHelpers'; + +export enum ModEnabledState { + unmanaged = '*', + enabled = '+', + disabled = '-', + unknown = '?', +} + +export class ModListItem { + public name: string = ''; + public enabled: ModEnabledState = ModEnabledState.unmanaged; + constructor(name: string, enabled: ModEnabledState) { + this.name = name; + this.enabled = enabled; + } +} + +export const InstanceIniName = 'ModOrganizer.ini'; +export const MO2EXEName = 'ModOrganizer.exe'; + +export interface MO2CustomExecutableInfo { + arguments: string; + binary: string; + hide: boolean; + ownicon: boolean; + steamAppID: string; + title: string; + toolbar: boolean; + workingDirectory: string; +} +export type MO2LongGameID = + | 'Fallout 4' + | 'Skyrim Special Edition' + | 'Skyrim' + | 'Enderal' + | 'Fallout 3' + | 'Fallout 4 VR' + | 'New Vegas' + | 'Morrowind' + | 'Skyrim VR' + | 'TTW' + | 'Other'; +export type MO2ShortGameID = 'Fallout4' | 'SkyrimSE' | 'Skyrim'; //TODO: Do the rest of these | 'enderal' | 'fo3' | 'fo4vr' | 'nv' | 'morrowind' | 'skyrimvr' | 'ttw' | 'other'; + +export interface MO2InstanceInfo { + name: string; + gameName: MO2LongGameID; + gameDirPath: string; + selectedProfile: string; + iniPath: string; + baseDirectory: string; + downloadsFolder: string; + modsFolder: string; + cachesFolder: string; + profilesFolder: string; + overwriteFolder: string; + customExecutables: MO2CustomExecutableInfo[]; +} + +// import a library that deals with nukpg version strings + +// import the semver library +import * as semver from 'semver'; + +export interface WinVerObject { + major: number; + minor: number; + build: number; + privateNum: number; + version: string; +} + +export class WinVer implements WinVerObject { + public readonly major: number = 0; + public readonly minor: number = 0; + public readonly build: number = 0; + public readonly privateNum: number = 0; + public readonly version: string = '0.0.0.0'; + public static fromVersionString(version: string): WinVer { + let parts = version.split('.'); + let major = parseInt(parts[0]); + let minor = parseInt(parts[1]); + let build = parseInt(parts[2]); + let privateNum = parseInt(parts[3]); + return new WinVer({ major, minor, build, privateNum, version }); + } + public static lessThan(a: WinVerObject, b: WinVerObject): boolean { + return a.major < b.major || a.minor < b.minor || a.build < b.build || a.privateNum < b.privateNum; + } + public static equal(a: WinVerObject, b: WinVerObject): boolean { + return a.major === b.major && a.minor === b.minor && a.build === b.build && a.privateNum === b.privateNum; + } + public static greaterThan(a: WinVerObject, b: WinVerObject): boolean { + return a.major > b.major || a.minor > b.minor || a.build > b.build || a.privateNum > b.privateNum; + } + public static greaterThanOrEqual(a: WinVerObject, b: WinVerObject): boolean { + return WinVer.greaterThan(a, b) || WinVer.equal(a, b); + } + public lt(other: WinVer): boolean { + return WinVer.lessThan(this, other); + } + public eq(other: WinVer): boolean { + return WinVer.equal(this, other); + } + public gt(other: WinVer): boolean { + return WinVer.greaterThan(this, other); + } + constructor(iWinVer: WinVerObject) { + this.major = iWinVer.major; + this.minor = iWinVer.minor; + this.build = iWinVer.build; + this.privateNum = iWinVer.privateNum; + this.version = iWinVer.version; + } +} + +export interface MO2ModMetaInstalledFile { + modid: number; + fileid: number; +} + +export interface MO2ModMeta { + modid: number; + version: string; + newestVersion: string; + category: string; + installedFiles: MO2ModMetaInstalledFile[]; + nexusFileStatus?: number; + gameName?: MO2ShortGameID; + ignoredVersion?: string; + installationFile?: string; + repository?: string; + comments?: string; + notes?: string; + nexusDescription?: string; + url?: string; + hasCustomURL?: boolean; + lastNexusQuery?: string; + lastNexusUpdate?: string; + nexusLastModified?: string; + converted?: boolean; + validated?: boolean; + color?: string; + endorsed?: number; + tracked?: number; +} + +export interface MO2Location { + MO2EXEPath: string; + instances: MO2InstanceInfo[]; + isPortable: boolean; +} + +export function GetGlobalMO2DataFolder(): string | undefined { + let appdata = getLocalAppDataFolder(); + if (appdata === undefined) { + return undefined; + } + return path.join(appdata, 'ModOrganizer'); +} + +export async function IsMO2Portable(MO2EXEPath: string): Promise { + const basedir = path.dirname(MO2EXEPath); + const portableSigil = path.join(basedir, 'portable.txt'); + if (existsSync(portableSigil)) { + return true; + } + return false; +} + +export async function GetMO2EXELocations(gameId?: MO2LongGameID, ...additionalIds: MO2LongGameID[]): Promise { + let possibleLocations: string[] = []; + let nxmHandlerIniPath = await FindNXMHandlerIniPath(); + if (nxmHandlerIniPath === undefined) { + return possibleLocations; + } + let MO2NXMData = await ParseIniFile(nxmHandlerIniPath); + if (MO2NXMData === undefined) { + return possibleLocations; + } + possibleLocations = GetMO2EXELocationsFromNXMHandlerData(MO2NXMData, gameId, ...additionalIds); + // Filter all the ones that don't exist + return possibleLocations.filter((value) => fs.existsSync(value)); +} + +function getIDFromNXMHandlerName(nxmName: string): MO2LongGameID | undefined { + let _nxmName = nxmName.toLowerCase().replace(/ /g, ''); + switch (_nxmName) { + case 'skyrimse': + case 'skyrimspecialedition': + return 'Skyrim Special Edition'; + case 'skyrim': + return 'Skyrim'; + case 'fallout4': + return 'Fallout 4'; + case 'enderal': + return 'Enderal'; + case 'fallout3': + return 'Fallout 3'; + case 'fallout4vr': + return 'Fallout 4 VR'; + case 'falloutnv': + case 'newvegas': + return 'New Vegas'; + case 'morrowind': + return 'Morrowind'; + case 'skyrimvr': + return 'Skyrim VR'; + case 'ttw': + return 'TTW'; + case 'other': + return 'Other'; + default: + return undefined; + } +} + +/** + * ModOrganizer2 installs a nxmhandler.ini file in the global data folder. + * This conveniently has a list of all the MO2 installations (even the portable ones) + * and their associated game(s). + * + * @param nxmData + * @param gameID - The game ID to filter by + * @param additionalIds - Additional game IDs to filter by + */ +function GetMO2EXELocationsFromNXMHandlerData( + nxmData: INIData, + gameId?: MO2LongGameID, + ...additionalIds: MO2LongGameID[] +): string[] { + let exePaths: string[] = []; + if (!nxmData.handlers) { + return exePaths; + } + let handler_array = ParseIniArray(nxmData.handlers); + if (!handler_array || handler_array.length === 0) { + return exePaths; + } + for (const handler of handler_array) { + let executable: string | undefined = handler.executable; + let games: string | undefined = handler.games; + if (!executable || !games) { + continue; + } + let gameList = NormalizeIniString(games) + .split(',') + .filter((val) => val !== ''); + if (!gameId) { + exePaths.push(executable); + } else { + let _args = [gameId, ...additionalIds]; + for (const gameID of _args) { + if ( + gameList.filter((val) => { + let _valID = getIDFromNXMHandlerName(val); + if (_valID === undefined) { + return false; + } + return _valID === gameID; + }).length > 0 + ) { + exePaths.push(executable); + break; + } + } + } + } + + // filter out non-uniques + // We do it after the above because duplicate paths may have different `games` values + exePaths = exePaths.filter((value, index, self) => { + return self.indexOf(value) === index; + }); + return exePaths; +} +function NormalizeIniString(str: string) { + let _str = str; + if (_str.startsWith('@ByteArray(') && _str.endsWith(')')) { + _str = _str.substring(11, _str.length - 1); + } + // replace all '\"' with '"' + _str = _str.replace(/\\"/g, '"'); + // replace all '\\' with '\' + _str = _str.replace(/\\\\/g, '\\'); + return _str; +} +function NormalizeIniPathString(pathstring: string) { + let _str = NormalizeIniString(pathstring); + // remove all leading and trailing quotes + if (_str.startsWith('\\"') && _str.endsWith('\\"')) { + _str = _str.substring(2, _str.length - 2); + } + if (_str.startsWith('"') && _str.endsWith('"')) { + _str = _str.substring(1, _str.length - 1); + } + return _normalizePath(_str); +} +function NormalizeMO2IniPathString(pathstring: string, basedir: string) { + return _normalizePath(NormalizeIniPathString(pathstring)?.replace(/%BASE_DIR%/g, _normalizePath(basedir) + '/')); +} +function NormalizePath(pathstring: string): string { + return _normalizePath(pathstring) || ''; +} +function _normalizePath(pathstring: string | undefined) { + return pathstring === undefined ? undefined : path.normalize(pathstring).replace(/\\/g, '/'); +} + +function _normInistr(str: string | undefined): string | undefined { + return str === undefined ? undefined : NormalizeIniString(str); +} + +function _normIniPath(pathstring: string | undefined): string | undefined { + return pathstring === undefined ? undefined : NormalizeIniPathString(pathstring); +} + +function _normMO2Path(pathstring: string | undefined, basedir: string | undefined): string | undefined { + return pathstring === undefined || basedir === undefined + ? undefined + : NormalizeMO2IniPathString(pathstring, basedir); +} + +function ParseMO2CustomExecutable(iniData: INIData) { + let title = _normInistr(iniData.title); + let binary = _normIniPath(iniData.binary); + let steamAppID = _normInistr(iniData.steamAppID) || ''; + let toolbar = iniData.toolbar === true; // explicit boolean check in case unset + let hide = iniData.hide === true; + let ownicon = iniData.ownicon === true; + let arguments_ = _normInistr(iniData.arguments) || ''; + let workingDirectory = _normIniPath(iniData.workingDirectory) || ''; + if (title !== undefined && binary !== undefined) { + let result: MO2CustomExecutableInfo = { + arguments: arguments_, + binary: binary, + hide: hide, + ownicon: ownicon, + steamAppID: steamAppID, + title: title, + toolbar: toolbar, + workingDirectory: workingDirectory, + }; + return result; + } + return undefined; +} + +function ParseMO2CustomExecutables(iniArray: INIData[]) { + let result: MO2CustomExecutableInfo[] = []; + for (const iniData of iniArray) { + let parsed = ParseMO2CustomExecutable(iniData); + if (parsed) { + result.push(parsed); + } + } + return result; +} + +/** + * Parses the data from a ModOrganizer.ini file and returns the information needed + * + * The ini data is structured like this: + * ```none + * [General] + * gameName=Skyrim Special Edition + * selected_profile=@ByteArray(Default) + * gamePath=@ByteArray(D:\\Games\\Skyrim Special Edition) + * version=2.4.4 + * first_start=false + * <...> + * [Settings] + * <...> + * base_directory="D:\\ModsForSkyrim" + * mod_directory=D:\ModsForSkyrim\mods + * ``` + * + * gameName is the long MO2 identifier of the game, like "Skyrim Special Edition"; it's set by MO2, it's not possible to be changed by the user + * + * base_directory doesn't get set if the ModOrganizer.ini file is in the base directory + * + * the various _directory values don't get set if they do not differ from %BASE_DIR%/{value} + * + * @param iniPath - path to the ModOrganizer.ini file + * @param iniData - data from the ModOrganizer.ini file + * @returns + */ +function ParseInstanceINI(iniPath: string, iniData: INIData, isPortable: boolean): MO2InstanceInfo | undefined { + let iniBaseDir = NormalizePath(path.dirname(iniPath)); + let instanceName = isPortable ? 'portable' : path.basename(iniBaseDir); + let gameName: MO2LongGameID = iniData.General['gameName']; + if (gameName === undefined) { + return undefined; + } + let gameDirPath = _normIniPath(iniData.General['gamePath']); + if (gameDirPath === undefined) { + return undefined; + } + // TODO: We should probably pin to a specific minor version of MO2 + let version = iniData.General['version']; + + // TODO: Figure out if this is ever not set + let selectedProfile = _normInistr(iniData.General['selected_profile']) || 'Default'; + + let settings = iniData.Settings || {}; // Settings may be empty; we don't need it to populate the rest of the information + + let baseDirectory = _normIniPath(settings['base_directory']) || iniBaseDir; + let downloadsPath = + _normMO2Path(settings['download_directory'], baseDirectory) || path.join(baseDirectory, 'downloads'); + let modsPath = _normMO2Path(settings['mod_directory'], baseDirectory) || path.join(baseDirectory, 'mods'); + let cachesPath = _normMO2Path(settings['cache_directory'], baseDirectory) || path.join(baseDirectory, 'webcache'); + let profilesPath = + _normMO2Path(settings['profiles_directory'], baseDirectory) || path.join(baseDirectory, 'profiles'); + let overwritePath = + _normMO2Path(settings['overwrite_directory'], baseDirectory) || path.join(baseDirectory, 'overwrite'); + let customExecutables: MO2CustomExecutableInfo[] = []; + if (iniData.customExecutables) { + let arr = ParseIniArray(iniData.customExecutables); + if (arr && arr.length > 0) { + customExecutables = ParseMO2CustomExecutables(arr); + } + } + return { + name: instanceName, + gameName: gameName, + gameDirPath: gameDirPath, + customExecutables: customExecutables, + selectedProfile: selectedProfile, + iniPath: iniPath, + baseDirectory: baseDirectory, + downloadsFolder: downloadsPath, + modsFolder: modsPath, + cachesFolder: cachesPath, + profilesFolder: profilesPath, + overwriteFolder: overwritePath, + }; +} + +/** + * Parses the data from a ModOrganizer.ini file and returns an MO2InstanceInfo object + * @param iniPath - path to the ModOrganizer.ini file + * @returns MO2InstanceInfo + */ +export async function GetMO2InstanceInfo(iniPath: string): Promise { + let iniData = await ParseIniFile(iniPath); + if (iniData === undefined) { + return undefined; + } + return ParseInstanceINI(iniPath, iniData, await IsMO2Portable(iniPath)); +} + +export async function validateInstanceLocationInfo(info: MO2InstanceInfo): Promise { + // check that all the directory paths exist and they are directories + let dirPaths = [ + info.gameDirPath, + info.baseDirectory, + info.downloadsFolder, + info.modsFolder, + info.cachesFolder, + info.profilesFolder, + info.overwriteFolder, + ]; + for (let p of dirPaths) { + if (!existsSync(p)) { + return false; + } + //check if p is a directory + if (!fs.statSync(p).isDirectory()) { + return false; + } + } + if (!existsSync(info.iniPath) || !fs.statSync(info.iniPath).isFile()) { + return false; + } + return true; +} + +export async function FindInstanceForEXE(MO2EXEPath: string, instanceName?: string) { + if (!fs.existsSync(MO2EXEPath)) { + return undefined; + } + let isPortable = await IsMO2Portable(MO2EXEPath); + if (isPortable) { + let instanceFolder = path.dirname(MO2EXEPath); + let instanceIniPath = path.join(instanceFolder, InstanceIniName); + return await GetMO2InstanceInfo(instanceIniPath); + } else if (instanceName !== undefined && instanceName !== 'portable') { + return await FindGlobalInstance(instanceName); + } + return undefined; +} + +function _portableExeIni(exePath: string): string { + return path.join(path.dirname(exePath), InstanceIniName); +} + +export async function GetLocationInfoForEXE( + MO2EXEPath: string, + gameId?: MO2LongGameID, + ...addtionalIds: MO2LongGameID[] +): Promise { + if (!fs.existsSync(MO2EXEPath)) { + return undefined; + } + let isPortable = await IsMO2Portable(MO2EXEPath); + let instanceInfos: MO2InstanceInfo[] = []; + if (isPortable) { + let instanceInfo = await GetMO2InstanceInfo(_portableExeIni(MO2EXEPath)); + instanceInfos = instanceInfo ? [instanceInfo] : []; + } else { + instanceInfos = await FindGlobalInstances(gameId, ...addtionalIds); + } + if (instanceInfos.length === 0) { + return undefined; + } + return { + MO2EXEPath: MO2EXEPath, + isPortable: isPortable, + instances: instanceInfos, + }; +} + +export async function FindAllKnownMO2EXEandInstanceLocations( + gameID?: MO2LongGameID, + ...additionalIds: MO2LongGameID[] +): Promise { + let possibleLocations: MO2Location[] = []; + let exeLocations = await GetMO2EXELocations(gameID, ...additionalIds); + if (exeLocations.length !== 0) { + let globalInstances = (await FindGlobalInstances(gameID)) || []; + for (let exeLocation of exeLocations) { + let instanceInfos: MO2InstanceInfo[] | undefined = undefined; + let isPortable = await IsMO2Portable(exeLocation); + if (isPortable) { + let instanceInfo = await GetMO2InstanceInfo(_portableExeIni(exeLocation)); + instanceInfos = instanceInfo ? [instanceInfo] : []; + } else { + instanceInfos = globalInstances; + } + if (instanceInfos.length === 0) { + continue; + } + possibleLocations.push({ + MO2EXEPath: exeLocation, + instances: instanceInfos, + isPortable: isPortable, + }); + } + } + return possibleLocations; +} + +function GetNXMHandlerIniPath(): string | undefined { + let global = GetGlobalMO2DataFolder(); + if (global === undefined) { + return undefined; + } + return path.join(global, 'nxmhandler.ini'); +} + +export async function FindNXMHandlerIniPath(): Promise { + let nxmHandlerIniPath = GetNXMHandlerIniPath(); + if (nxmHandlerIniPath === undefined || !existsSync(nxmHandlerIniPath)) { + return undefined; + } + return nxmHandlerIniPath; +} + +export async function IsInstanceOfGame(gameID: MO2LongGameID, instanceIniPath: string): Promise { + let iniData = await ParseIniFile(instanceIniPath); + if (iniData === undefined) { + return false; + } + return _isInstanceOfGames(iniData, gameID); +} + +function _isInstanceOfGames( + instanceIniData: INIData, + gameID: MO2LongGameID, + ...additionalIds: MO2LongGameID[] +): boolean { + if (instanceIniData.General === undefined || instanceIniData.General.gameName === undefined) { + return false; + } + + let gameIDs = [gameID, ...additionalIds]; + for (let id of gameIDs) { + if (instanceIniData.General.gameName === id) { + return true; + } + } + return false; +} + +export async function FindGlobalInstance(name: string): Promise { + let globalFolder = GetGlobalMO2DataFolder(); + if (globalFolder === undefined || (!existsSync(globalFolder) && !fs.statSync(globalFolder).isDirectory())) { + return undefined; + } + let instanceNames = readdirSync(globalFolder, { withFileTypes: true }); + let instance = instanceNames.find((dirent) => dirent.isDirectory() && dirent.name === name); + if (instance === undefined) { + return undefined; + } + let instanceIniPath = path.join(globalFolder, instance.name, InstanceIniName); + let iniData = await ParseIniFile(instanceIniPath); + if (!iniData) { + return undefined; + } + return ParseInstanceINI(instanceIniPath, iniData, false); +} + +export async function FindGlobalInstances( + gameId?: MO2LongGameID, + ...additionalIds: MO2LongGameID[] +): Promise { + let possibleLocations: MO2InstanceInfo[] = []; + let globalFolder = GetGlobalMO2DataFolder(); + // list all the directories in globalMO2Data + if (globalFolder === undefined || (!existsSync(globalFolder) && !fs.statSync(globalFolder).isDirectory())) { + return []; + } + let instanceNames = readdirSync(globalFolder, { withFileTypes: true }); + for (let dirent of instanceNames) { + if (dirent.isDirectory()) { + let instanceIniPath = path.join(globalFolder, dirent.name, InstanceIniName); + let iniData = await ParseIniFile(instanceIniPath); + if (!iniData) { + continue; + } + if (gameId !== undefined && !_isInstanceOfGames(iniData, gameId, ...additionalIds)) { + continue; + } + let info = ParseInstanceINI(instanceIniPath, iniData, false); + if (info !== undefined) { + possibleLocations.push(info); + } + } + } + return possibleLocations; +} + +export async function GetCurrentGlobalInstance(): Promise { + // HKEY_CURRENT_USER\SOFTWARE\Mod Organizer Team\Mod Organizer\CurrentInstance + let currentInstanceName = await getRegistryValueData( + 'SOFTWARE\\Mod Organizer Team\\Mod Organizer', + 'CurrentInstance', + 'HKCU' + ); + if (currentInstanceName) { + return FindGlobalInstance(currentInstanceName); + } + return undefined; +} + +/** + * Parse modlist.txt file contents from Mod Organizer 2 + * + * The format is: + * - Mod names are the names of their directories in the mods folder + * i.e. "SkyUI", not "SkyUI.esp", with an exception for official DLC, which is prefixed with "DLC: " + * - Comments are prefixed with '#' + * - Enabled mods are prefixed with "+" and disabled mods are prefixed with "-" + * - Unmanaged mods (e.g. Game DLC) are prefixed with "*" + * - Seperators are prefixed with "-" and have the suffix "_separator" + * - The mods are loaded in order. + * - Any mods listed earlier will overwrite files in mods listed later. + * They appear in the ModOrganizer gui in reverse order (i.e. the last mod in the file is the first mod in the gui) + * + * Example of a modlist.txt file: + * ```none + * # This file was automatically generated by Mod Organizer. + * +Unofficial Skyrim Special Edition Patch + * -SkyUI + * +Immersive Citizens - AI Overhaul + * *DLC: Automatron + * +Immersive Citizens - OCS patch + * -Auto Loot SE + * *DLC: Far Harbor + * *DLC: Contraptions Workshop + * ``` + * This function returns an array of mod list items in the order they appear in the file + */ + +export function ParseModListText(modListContents: string): ModListItem[] { + let modlist = new Array(); + const modlistLines = modListContents.replace(/\r\n/g, '\n').split('\n'); + for (let line of modlistLines) { + if (line.charAt(0) === '#' || line === '') { + continue; + } + let indic = line.charAt(0); + let modName = line.substring(1); + let modEnabledState: ModEnabledState | undefined = undefined; + switch (indic) { + case '+': + modEnabledState = ModEnabledState.enabled; + break; + case '-': + modEnabledState = ModEnabledState.disabled; + break; + case '*': + modEnabledState = ModEnabledState.unmanaged; + break; + } + if (modEnabledState === undefined) { + // skip this line + continue; + } + modlist.push(new ModListItem(modName, modEnabledState)); + } + return modlist; +} + +export async function ParseModListFile(modlistPath: string): Promise { + // create an ordered map of mod names to their enabled state + if (!fs.existsSync(modlistPath) || !fs.lstatSync(modlistPath).isFile()) { + return undefined; + } + const modlistContents = readFileSync(modlistPath, 'utf8').replace(/\r\n/g, '\n'); + if (!modlistContents) { + return undefined; + } + return ParseModListText(modlistContents); +} +// parse moshortcut URI + +export function parseMoshortcutURI(moshortcutURI: string): { instanceName: string; exeName: string } { + let moshortcutparts = moshortcutURI.replace('moshortcut://', '').split(':'); + let instanceName = moshortcutparts[0] || 'portable'; + let exeName = moshortcutparts[1]; + return { instanceName, exeName }; +} + +export function checkIfModExistsAndEnabled(modlist: Array, modName: string) { + return modlist.findIndex((mod) => mod.name === modName && mod.enabled === ModEnabledState.enabled) !== -1; +} + +/** + * If we find the mod in the modlist, remove it and return the new modlist + * @param modlist + * @param modName + * @returns + */ + + +export function IndexOfModList(modlist: Array, modName: string) { + return modlist.findIndex((m) => m.name === modName); +} + +export function RemoveMod(modlist: Array, modName: string) { + let modIndex = modlist.findIndex((m) => m.name === modName); + if (modIndex !== -1) { + return modlist.slice(0, modIndex).concat(modlist.slice(modIndex + 1)); + } + return modlist; +} + +export function AddModToBeginningOfModList(modlist: Array, mod: ModListItem) { + // check if the mod is already in the modlist + let modIndex = modlist.findIndex((m) => m.name === mod.name); + if (modIndex !== -1) { + // if the mod is already in the modlist, remove it and return the modlist with the specified mod at the top + return [mod].concat(modlist.slice(0, modIndex).concat(modlist.slice(modIndex + 1))); + } + return [mod].concat(modlist); +} + +export function AddModIfNotInModList(modlist: Array, mod: ModListItem) { + // check if the mod is already in the modlist + let modIndex = IndexOfModList(modlist, mod.name); + if (modIndex === -1) { + // if the mod is not already in the modlist, add it at the beginning + return [mod].concat(modlist); + } + // otherwise just return it + return modlist; +} + +export function AddOrEnableModInModList(modlist: Array, modName: string) { + let modIndex = modlist.findIndex((m) => m.name === modName); + if (modIndex !== -1) { + return modlist.slice(0, modIndex).concat( + new ModListItem(modName, ModEnabledState.enabled), + modlist.slice(modIndex + 1)); + } + return AddModToBeginningOfModList(modlist, new ModListItem(modName, ModEnabledState.enabled)); +} + +// modlist.txt has to be in CRLF, because MO2 is cursed +export function ModListToText(modlist: Array) { + let modlistText = '# This file was automatically generated by Mod Organizer.\r\n'; + for (let mod of modlist) { + modlistText += mod.enabled + mod.name + '\r\n'; + } + return modlistText; +} + +export function WriteChangesToModListFile(modlistPath: string, modlist: Array) { + let modlistContents = ModListToText(modlist); + fs.rmSync(modlistPath, { force: true }); + if (!openSync(modlistPath, 'w')) { + return false; + } + writeFileSync(modlistPath, modlistContents, 'utf8'); + return true; +} + +/** + * MO2 handles exe arguments awfully, which is why we have to do this tortured parsing. + * Don't use this for anything other than MO2, because it's not a general purpose parser + * Note: This is not used for parsing the custom executable objects; args are stored as the literal there + * + * In MO2, this is just a string, and the only argument passed to an executable is that string, instead of as an array of arguments. + * cmd.exe ends up mangling the arguments if they contain quote-literals. + */ +export function ParseMO2CmdLineArguments(normargstring: string) { + let args: string[] = []; + let arg = ''; + let inQuote = false; + for (let i = 0; i < normargstring.length; i++) { + const char = normargstring[i]; + // if we hit a space, and we're not in a quote, then we've hit the end of an argument + if (char === ' ') { + if (!inQuote && arg.length > 0) { + args.push(arg); + arg = ''; + } else { + arg += char; + } + } else if (char === '"') { + // if we hit a quote, and we're not in a quote, then we're starting a quote + if (inQuote === false) { + // If the arg started, add the quote to it + if (arg.length > 0) { + arg += char; + } + // Even if the above is true, we're still starting a quote + inQuote = true; + } else { + // if we hit a quote, and we're in a quote, then we're ending a quote + inQuote = false; + // peek ahead to see if the next character is a space + if (i !== normargstring.length - 1 && normargstring[i + 1] !== ' ') { + arg += char; + } + + // if the argument already has a quote literal in it, then we need to add the quote to the arg + else if (arg.indexOf('"') !== -1) { + arg += char; + } + } + } else { + arg += char; + } + } + // get the last one if there was one + if (arg.length > 0 && arg.trim() !== '') { + args.push(arg); + } + return args; +} +/*** + * Format is like this: + * ```none + * [General] + * gameName=Fallout4 + * modid=47327 + * ignoredVersion= + * version=1.10.163.0 + * newestVersion=1.10.163.0 + * category="35," + * nexusFileStatus=1 + * installationFile=Addres Library-47327-1-10-163-0-1599728753.zip + * repository=Nexus + * comments= + * notes= + * nexusDescription="This project is a resource for plugins developed using [url=https://github.com/Ryan-rsm-McKenzie/CommonLibF4]CommonLibF4.[/url]" + * url= + * hasCustomURL=false + * lastNexusQuery=2022-12-23T00:08:20Z + * lastNexusUpdate=2022-12-23T00:08:20Z + * nexusLastModified=2020-09-10T09:08:54Z + * converted=false + * validated=false + * color=@Variant(\0\0\0\x43\0\xff\xff\0\0\0\0\0\0\0\0) + * endorsed=0 + * tracked=0 + * + * [installedFiles] + * 1\modid=47327 + * 1\fileid=191018 + * size=1 + * ``` + * + * the only required fields are: + * - modid + * - version + * - newestVersion + * - category + * - installationFile + * - [installedFiles].size + */ + +export function isKeyOfObject( + key: string | number | symbol, + obj: T, + ): key is keyof T { + return key in obj; + } + +function ParseModMetaIni(modMetaIni: INIData): MO2ModMeta | undefined { + if (!modMetaIni) { + return undefined; + } + if (modMetaIni.farts === undefined) { + console.log('lmao'); + } + if ( + modMetaIni.General === undefined || + modMetaIni.General.modid === undefined || + modMetaIni.General.version === undefined || + modMetaIni.General.newestVersion === undefined || + modMetaIni.General.installationFile === undefined || + modMetaIni.General.category === undefined || + modMetaIni.installedFiles === undefined || + modMetaIni.installedFiles.size === undefined + ) { + return undefined; + } + let general = modMetaIni.General; + // check if each key in general is a key in the type MO2ModMeta + let modMeta = {} as any; + for (let key in general) { + if (isKeyOfObject(key, general as MO2ModMeta)) { + modMeta[key] = general[key]; + } + } + let installedFilesSize = modMetaIni.installedFiles.size; + if (!installedFilesSize) { + return undefined; + } + let installedFiles = ParseIniArray(modMetaIni.installedFiles); + if (!installedFiles) { + return undefined; + } + modMeta["installedFiles"] = installedFiles.map((installedFile) => { + return { + modid: installedFile.modid, + fileid: installedFile.fileid, + } as MO2ModMetaInstalledFile; + }); + return modMeta as MO2ModMeta; +} + +export function SerializeModMetaInfo(info: MO2ModMeta) { + let ini = {} as INIData; + ini.General = {} as INIData; + Object.keys(info).forEach((key) => { + if (key !== 'installedFiles' && info[key as keyof MO2ModMeta] !== undefined) { + ini.General[key] = info[key as keyof MO2ModMeta]; + } + }); + ini.installedFiles = SerializeIniArray(info.installedFiles); + return ini; +} + +export async function ParseModMetaIniFile(modMetaIniPath: string) { + let modMetaIni = await ParseIniFile(modMetaIniPath); + if (!modMetaIni) { + return undefined; + } + return ParseModMetaIni(modMetaIni); +} + +export function AddSeparatorToBeginningOfModList(name: string, modList: ModListItem[]): ModListItem[] { + return AddModIfNotInModList(modList, new ModListItem(name + "_separator", ModEnabledState.disabled)) +} diff --git a/src/papyrus-lang-vscode/src/common/constants.ts b/src/papyrus-lang-vscode/src/common/constants.ts index fab81d77..b59426e4 100644 --- a/src/papyrus-lang-vscode/src/common/constants.ts +++ b/src/papyrus-lang-vscode/src/common/constants.ts @@ -19,24 +19,3 @@ export enum AddressLibAssetSuffix { SkyrimAE = 'SkyrimAE', Fallout4 = 'Fallout4', } - -export function getAsssetLibraryDLSuffix(addlibname: AddressLibraryName): AddressLibAssetSuffix { - switch (addlibname) { - case AddressLibrarySKSEModName: - return AddressLibAssetSuffix.SkyrimSE; - case AddressLibrarySKSEAEModName: - return AddressLibAssetSuffix.SkyrimAE; - case AddressLibraryF4SEModName: - return AddressLibAssetSuffix.Fallout4 - } -} -export function getAddressLibNameFromAssetSuffix(suffix: AddressLibAssetSuffix): AddressLibraryName { - switch (suffix) { - case AddressLibAssetSuffix.SkyrimSE: - return AddressLibrarySKSEModName; - case AddressLibAssetSuffix.SkyrimAE: - return AddressLibrarySKSEAEModName; - case AddressLibAssetSuffix.Fallout4: - return AddressLibraryF4SEModName; - } -} diff --git a/src/papyrus-lang-vscode/src/debugger/AddLibHelpers.ts b/src/papyrus-lang-vscode/src/debugger/AddLibHelpers.ts new file mode 100644 index 00000000..5fd6475e --- /dev/null +++ b/src/papyrus-lang-vscode/src/debugger/AddLibHelpers.ts @@ -0,0 +1,383 @@ +import { GithubRelease } from '@terascope/fetch-github-release/dist/src/interfaces'; +import { AddressLibAssetSuffix, AddressLibraryName } from '../common/constants'; +import { getAddressLibNameFromAssetSuffix, getAddressLibNames, getAsssetLibraryDLSuffix } from '../common/GameHelpers'; +import { + DownloadAssetAndCheckHash, + downloadAssetFromGitHub, + DownloadResult, + GetLatestReleaseFromRepo, +} from '../common/GithubHelpers'; +import * as fs from 'fs'; +import { promisify } from 'util'; +import path from 'path'; +import extractZip from 'extract-zip'; +import { PapyrusGame } from '../PapyrusGame'; +import { CheckHashFile, GetHashOfFolder, mkdirIfNeeded } from '../Utilities'; +const exists = promisify(fs.exists); +const readFile = promisify(fs.readFile); +const lstat = promisify(fs.lstat); + +export const AddLibRepoUserName = 'nikitalita'; +export const AddLibRepoName = 'address-library-dist'; + +export interface AddressLibReleaseAssetList { + version: string; + // The name of the zip file + SkyrimSE: Asset; + // The name of the zip file + SkyrimAE: Asset; + // The name of the zip file + Fallout4: Asset; +} + +export interface Asset { + /** + * The file name of the zip file + */ + zipFile: string; + folderName: AddressLibraryName; + zipFileHash: string; + /** + * For checking if the installed folder has the same folder hash as the one we have + */ + folderHash: string; +} + +export function _getAsset(assetList: AddressLibReleaseAssetList, suffix: AddressLibAssetSuffix): Asset | undefined { + return assetList[suffix]; +} + +export function AddLibHelpers() {} +export function GetAssetZipForSuffixFromRelease( + release: GithubRelease, + name: AddressLibAssetSuffix +): string | undefined { + const _assets = release.assets.filter((asset) => asset.name.indexOf(name) >= 0); + if (_assets.length == 0) { + return undefined; + } else if (_assets.length > 1) { + // This should never happen + throw new Error('Too many assets found for suffix: ' + name + ''); + } + return _assets[0].name; +} + +export function getAssetListFromAddLibRelease(release: GithubRelease): AddressLibReleaseAssetList | undefined { + let assetZip: string | undefined | Error; + let ret: AddressLibReleaseAssetList = new Object() as AddressLibReleaseAssetList; + ret.version = release.tag_name; + for (let idx in AddressLibAssetSuffix) { + const assetSuffix: AddressLibAssetSuffix = AddressLibAssetSuffix[idx as keyof typeof AddressLibAssetSuffix]; + assetZip = GetAssetZipForSuffixFromRelease(release, assetSuffix); + if (!assetZip) { + return undefined; + } + ret[assetSuffix] = { + zipFile: assetZip, + folderName: getAddressLibNameFromAssetSuffix(assetSuffix), + zipFileHash: '0', + folderHash: '0', + }; + } + return ret; +} + +export async function getLatestAddLibReleaseInfo(): Promise { + let latestReleaseInfo: GithubRelease | undefined; + try { + latestReleaseInfo = await GetLatestReleaseFromRepo(AddLibRepoUserName, AddLibRepoName, false); + if (!latestReleaseInfo) { + return undefined; + } + } catch (e) { + return undefined; + } + return latestReleaseInfo; +} + +interface CancellationToken { + isCancellationRequested: boolean; + onCancellationRequested: any; +} + +export async function DownloadLatestAddressLibs( + downloadFolder: string, + AssetListDLPath: string, + cancellationToken: CancellationToken +) { + const latestReleaseInfo = await getLatestAddLibReleaseInfo(); + if (!latestReleaseInfo) { + return DownloadResult.repoFailure; + } + const assetList = getAssetListFromAddLibRelease(latestReleaseInfo); + if (!assetList) { + return DownloadResult.repoFailure; + } + const release_id = latestReleaseInfo.id; + // get the shasums + let sha256SumsPath: string | undefined; + try { + sha256SumsPath = await downloadAssetFromGitHub( + AddLibRepoUserName, + AddLibRepoName, + release_id, + 'SHA256SUMS.json', + downloadFolder + ); + } catch (e) { + return DownloadResult.sha256sumDownloadFailure; + } + if (!sha256SumsPath || !(await exists(sha256SumsPath)) || !(await lstat(sha256SumsPath)).isFile()) { + return DownloadResult.sha256sumDownloadFailure; + } + const sha256buf = await readFile(sha256SumsPath, 'utf8'); + if (!sha256buf) { + return DownloadResult.sha256sumDownloadFailure; + } + const sha256Sums = JSON.parse(sha256buf); + const retryLimit = 3; + let retries = 0; + for (let idx in AddressLibAssetSuffix) { + const assetSuffix: AddressLibAssetSuffix = AddressLibAssetSuffix[idx as keyof typeof AddressLibAssetSuffix]; + + if (cancellationToken.isCancellationRequested) return DownloadResult.cancelled; + + retries = 0; + const asset = _getAsset(assetList, assetSuffix); + if (!asset) { + return DownloadResult.repoFailure; + } + const expectedHash = sha256Sums[asset.zipFile]; + if (!expectedHash) { + return DownloadResult.repoFailure; + } + + let ret: DownloadResult = DownloadResult.downloadFailure; + while (retries < retryLimit && cancellationToken.isCancellationRequested == false) { + ret = await DownloadAssetAndCheckHash( + AddLibRepoUserName, + AddLibRepoName, + release_id, + asset.zipFile, + downloadFolder, + expectedHash + ); + if (ret == DownloadResult.success) { + break; + } + retries++; + } + + if (cancellationToken.isCancellationRequested) return DownloadResult.cancelled; + + if (ret != DownloadResult.success) { + return ret; + } + asset.zipFileHash = expectedHash; + const zipFilePath = path.join(downloadFolder, asset.zipFile); + // We extract the zip here to check the hash of the folder when we check the install state + // We don't end up installing from the folder, we install from the zip + const ExtractedFolderPath = path.join(downloadFolder, asset.folderName); + fs.rmSync(ExtractedFolderPath, { recursive: true, force: true }); + await extractZip(zipFilePath, { dir: ExtractedFolderPath }); + if (cancellationToken.isCancellationRequested) return DownloadResult.cancelled; + if (!(await _checkAddlibExtracted(asset.folderName, downloadFolder))) { + return DownloadResult.filesystemFailure; + } + let folderHash = await GetHashOfFolder(ExtractedFolderPath); + if (!folderHash) { + return DownloadResult.filesystemFailure; + } + asset.folderHash = folderHash; + if (cancellationToken.isCancellationRequested) return DownloadResult.cancelled; + // Remove it, because we don't install from it + fs.rmSync(ExtractedFolderPath, { recursive: true, force: true }); + assetList[assetSuffix] = asset; + } + // we do this last to make sure we don't write a corrupt json file + fs.writeFileSync(AssetListDLPath, JSON.stringify(assetList)); + return DownloadResult.success; +} + +export async function GetAssetList(jsonPath: string) { + if (!fs.existsSync(jsonPath) || !fs.lstatSync(jsonPath).isFile()) { + return undefined; + } + const contents = fs.readFileSync(jsonPath, 'utf8'); + if (!contents || contents.length == 0) { + // json is corrupt + return undefined; + } + const assetList = JSON.parse(contents) as AddressLibReleaseAssetList; + if (!assetList) { + // json is corrupt + return undefined; + } + // check integrity + for (let idx in AddressLibAssetSuffix) { + const assetSuffix: AddressLibAssetSuffix = AddressLibAssetSuffix[idx as keyof typeof AddressLibAssetSuffix]; + const currentAsset = _getAsset(assetList, assetSuffix); + if (!currentAsset) { + // json is corrupt + return undefined; + } + if ( + !currentAsset.zipFile || + !currentAsset.zipFileHash || + !currentAsset.folderName || + !currentAsset.folderHash + ) { + // json is corrupt + return undefined; + } + } + return assetList; +} + +export async function _checkDownloadIntegrity( + downloadpath: string, + assetList: AddressLibReleaseAssetList +): Promise { + if (!assetList) { + return false; + } + for (let idx in AddressLibAssetSuffix) { + const assetSuffix: AddressLibAssetSuffix = AddressLibAssetSuffix[idx as keyof typeof AddressLibAssetSuffix]; + const currentAsset = _getAsset(assetList, assetSuffix); + if (!currentAsset) { + return false; + } + const assetName = currentAsset.zipFile; + const assetPath = path.join(downloadpath, assetName); + if (!(await exists(assetPath))) { + return false; + } + if (!CheckHashFile(assetPath, currentAsset.zipFileHash)) { + return false; + } + } + return true; +} + +/** + * This checks to see if the ${modsDir}/${name}/{SK,F4}SE/Plugins folder has at least one file in it + * @param name - the name of the address library + * @param modsDir - the mods directory to check in + * @returns true or false + */ +export async function _checkAddlibExtracted(name: AddressLibraryName, modsDir: string): Promise { + const addressLibInstallPath = path.join(modsDir, name); + // TODO: refactor this + const SEDIR = name.indexOf('SKSE') >= 0 ? 'SKSE' : 'F4SE'; + const pluginsdir = path.join(addressLibInstallPath, SEDIR, 'Plugins'); + + if (!fs.existsSync(pluginsdir) || !fs.lstatSync(pluginsdir).isDirectory()) { + return false; + } + const files = fs.readdirSync(pluginsdir, { withFileTypes: true }); + if (files.length == 0) { + return false; + } + return true; +} +enum AddressLibInstalledState { + notInstalled, + installed, + outdated, + installedButCantCheckForUpdates, +} +/** + * Gets the state of the address library install + * + * @param name The name of the address library + * @param modsDir The mods directory to check in + * @param assetList The downloaded Address Library asset list to check against. + * If not provided, we don't check if it's outdated + * @returns AddressLibInstalledState + */ +export async function _checkAddressLibInstalled( + name: AddressLibraryName, + modsDir: string, + assetList?: AddressLibReleaseAssetList +): Promise { + if (!(await _checkAddlibExtracted(name, modsDir))) { + return AddressLibInstalledState.notInstalled; + } + if (assetList) { + const addressLibInstallPath = path.join(modsDir, name); + const suffix = getAsssetLibraryDLSuffix(name); + const asset = _getAsset(assetList, suffix); + if (!asset) { + throw new Error('Asset list is corrupt'); + } + const folderHash = await GetHashOfFolder(addressLibInstallPath); + if (!folderHash) { + return AddressLibInstalledState.notInstalled; + } + if (folderHash != asset.folderHash) { + return AddressLibInstalledState.outdated; + } + } + return AddressLibInstalledState.installed; +} + +/** + * @param game The game to check for + * @param modsDir The mods directory to check in + * @param assetList The downloaded Address Library asset list to check against. + * If not provided, we don't check if it's outdated + * @returns AddressLibInstalledState + */ +export async function _checkAddressLibsInstalled( + game: PapyrusGame, + modsDir: string, + assetList?: AddressLibReleaseAssetList +): Promise { + const addressLibFolderNames = getAddressLibNames(game); + for (let name of addressLibFolderNames) { + const state = await _checkAddressLibInstalled(name, modsDir, assetList); + if (state !== AddressLibInstalledState.installed) { + return state; + } + } + return AddressLibInstalledState.installed; +} + +export async function _installAddressLibs( + game: PapyrusGame, + ParentInstallDir: string, + downloadDir: string, + assetList: AddressLibReleaseAssetList, + cancellationToken: CancellationToken +): Promise { + const addressLibNames = getAddressLibNames(game); + for (let name of addressLibNames) { + if (cancellationToken.isCancellationRequested) { + return false; + } + const addressLibInstallPath = path.join(ParentInstallDir, name); + // The reason we check each individiual library is that Skyrim currently requires two libraries, + // So we want to make sure we don't overwrite one that is already installed + // We don't currently check if the library is outdated or not + const state = await _checkAddressLibInstalled(name, ParentInstallDir); + if (state === AddressLibInstalledState.installed) { + // It's installed and we're not forcing updates, so we don't need to do anything + continue; + } + const suffix = getAsssetLibraryDLSuffix(name); + const asset = _getAsset(assetList, suffix); + if (!asset) { + throw new Error('Asset list is corrupt'); + } + const zipPath = path.join(downloadDir, asset.zipFile); + fs.rmSync(addressLibInstallPath, { recursive: true, force: true }); + await mkdirIfNeeded(addressLibInstallPath); + await extractZip(zipPath, { + dir: addressLibInstallPath, + }); + if (!(await _checkAddressLibInstalled(name, ParentInstallDir))) { + return false; + } + } + return true; +} diff --git a/src/papyrus-lang-vscode/src/debugger/AddressLibInstallService.ts b/src/papyrus-lang-vscode/src/debugger/AddressLibInstallService.ts index a3e8c091..00f35048 100644 --- a/src/papyrus-lang-vscode/src/debugger/AddressLibInstallService.ts +++ b/src/papyrus-lang-vscode/src/debugger/AddressLibInstallService.ts @@ -1,38 +1,12 @@ import { inject, injectable, interfaces } from 'inversify'; -import { IExtensionConfigProvider } from '../ExtensionConfigProvider'; -import { CancellationToken, CancellationTokenSource } from 'vscode'; import { IPathResolver } from '../common/PathResolver'; import { PapyrusGame } from '../PapyrusGame'; -import { ILanguageClientManager } from '../server/LanguageClientManager'; - -import { mkdirIfNeeded } from '../Utilities'; - -import { GithubRelease } from '@terascope/fetch-github-release/dist/src/interfaces'; - -import * as path from 'path'; -import * as fs from 'fs'; -import { promisify } from 'util'; - -import md5File from 'md5-file'; -import { - AddressLibraryName, - AddressLibAssetSuffix, - getAddressLibNameFromAssetSuffix, - getAsssetLibraryDLSuffix, -} from '../common/constants'; -import extractZip from 'extract-zip'; import { - CheckHashFile, - DownloadAssetAndCheckHash, - downloadAssetFromGitHub, DownloadResult, - GetHashOfFolder, - GetLatestReleaseFromRepo, } from '../common/GithubHelpers'; -import { getAddressLibNames } from './MO2Helpers'; -const exists = promisify(fs.exists); -const copyFile = promisify(fs.copyFile); -const removeFile = promisify(fs.unlink); +import { CancellationToken, CancellationTokenSource } from 'vscode'; + +import * as AddLib from './AddLibHelpers' export enum AddressLibDownloadedState { notDownloaded, @@ -45,42 +19,13 @@ export enum AddressLibInstalledState { notInstalled, installed, outdated, - installedButCantCheckForUpdates -} - -export interface Asset { - /** - * The file name of the zip file - */ - zipFile: string; - folderName: AddressLibraryName; - zipFileHash: string; - /** - * For checking if the installed folder has the same folder hash as the one we have - */ - folderHash: string; -} -export interface AddressLibReleaseAssetList { - version: string; - // The name of the zip file - SkyrimSE: Asset; - // The name of the zip file - SkyrimAE: Asset; - // The name of the zip file - Fallout4: Asset; -} - -const AddLibRepoUserName = 'nikitalita'; -const AddLibRepoName = 'address-library-dist'; - -function _getAsset(assetList: AddressLibReleaseAssetList, suffix: AddressLibAssetSuffix): Asset | undefined { - return assetList[suffix]; + installedButCantCheckForUpdates, } export interface IAddressLibraryInstallService { getInstallState(game: PapyrusGame, modsDir?: string): Promise; getDownloadedState(): Promise; - DownloadLatestAddressLibs(cancellationToken: CancellationToken): Promise; + DownloadLatestAddressLibs(cancellationToken?: CancellationToken): Promise; installLibraries( game: PapyrusGame, forceDownload: boolean, @@ -94,172 +39,15 @@ export class AddressLibraryInstallService implements IAddressLibraryInstallServi private readonly _pathResolver: IPathResolver; constructor( - @inject(ILanguageClientManager) languageClientManager: ILanguageClientManager, - @inject(IExtensionConfigProvider) configProvider: IExtensionConfigProvider, @inject(IPathResolver) pathResolver: IPathResolver ) { this._pathResolver = pathResolver; } - private static _getAssetZipForSuffixFromRelease( - release: GithubRelease, - name: AddressLibAssetSuffix - ): string | undefined { - const _assets = release.assets.filter((asset) => asset.name.indexOf(name) >= 0); - if (_assets.length == 0) { - return undefined; - } else if (_assets.length > 1) { - // This should never happen - throw new Error('Too many assets found for suffix: ' + name + ''); - } - return _assets[0].name; - } - - private static getAssetListFromAddLibRelease(release: GithubRelease): AddressLibReleaseAssetList | undefined { - let assetZip: string | undefined | Error; - let ret: AddressLibReleaseAssetList; - ret.version = release.tag_name; - for (let _assetSuffix in AddressLibAssetSuffix) { - const assetSuffix = _assetSuffix as AddressLibAssetSuffix; - - assetZip = AddressLibraryInstallService._getAssetZipForSuffixFromRelease( - release, - assetSuffix - ); - if (!assetZip) { - return undefined; - } - ret[assetSuffix] = { - zipFile: assetZip, - folderName: getAddressLibNameFromAssetSuffix(assetSuffix), - zipFileHash: "0", - folderHash: "0", - }; - } - return ret; - } - - private static async getLatestAddLibReleaseInfo(): Promise { - let latestReleaseInfo: GithubRelease | undefined; - try { - latestReleaseInfo = await GetLatestReleaseFromRepo(AddLibRepoUserName, AddLibRepoName, false); - if (!latestReleaseInfo) { - return undefined; - } - } catch (e) { - return undefined; - } - return latestReleaseInfo; - } - - private static async _downloadLatestAddressLibs( - downloadFolder: string, - AssetListDLPath: string, - cancellationToken: CancellationToken - ) { - const latestReleaseInfo = await AddressLibraryInstallService.getLatestAddLibReleaseInfo(); - if (!latestReleaseInfo) { - return DownloadResult.repoFailure; - } - const assetList = AddressLibraryInstallService.getAssetListFromAddLibRelease(latestReleaseInfo); - if (!assetList) { - return DownloadResult.repoFailure; - } - const release_id = latestReleaseInfo.id; - // get the shasums - let sha256SumsPath: string; - try { - sha256SumsPath = await downloadAssetFromGitHub( - AddLibRepoUserName, - AddLibRepoName, - release_id, - 'SHA256SUMS.json', - downloadFolder - ); - if (!sha256SumsPath) { - return DownloadResult.sha256sumDownloadFailure; - } - } catch (e) { - return DownloadResult.sha256sumDownloadFailure; - } - - const sha256buf = fs.readFileSync(sha256SumsPath, 'utf8'); - if (!sha256buf) { - return DownloadResult.sha256sumDownloadFailure; - } - const sha256Sums = JSON.parse(sha256buf); - const retryLimit = 3; - let retries = 0; - for (const _assetSuffix in AddressLibAssetSuffix) { - const assetSuffix = _assetSuffix as AddressLibAssetSuffix; - if (cancellationToken.isCancellationRequested) { - return DownloadResult.cancelled; - } - retries = 0; - const asset = _getAsset(assetList, assetSuffix); - if (!asset) { - return DownloadResult.repoFailure; - } - const expectedHash = sha256Sums[asset.zipFile]; - if (!expectedHash) { - return DownloadResult.repoFailure; - } - let ret: DownloadResult = await DownloadAssetAndCheckHash( - AddLibRepoName, - AddLibRepoUserName, - release_id, - asset.zipFile, - downloadFolder, - expectedHash - ); - - while (retries < retryLimit && cancellationToken.isCancellationRequested == false) { - ret = await DownloadAssetAndCheckHash( - AddLibRepoName, - AddLibRepoUserName, - release_id, - asset.zipFile, - downloadFolder, - expectedHash - ); - if (ret == DownloadResult.success) { - break; - } - retries++; - } - if (cancellationToken.isCancellationRequested) { - return DownloadResult.cancelled; - } - if (ret != DownloadResult.success) { - return ret; - } - asset.zipFileHash = expectedHash; - const zipFilePath = path.join(downloadFolder, asset.zipFile); - // We extract the zip here to check the hash of the folder when we check the install state - // We don't end up installing from the folder, we install from the zip - const ExtractedFolderPath = path.join(downloadFolder, asset.folderName); - fs.rmSync(ExtractedFolderPath, { recursive: true, force: true }); - await extractZip(zipFilePath, { dir: ExtractedFolderPath }); - if (!await AddressLibraryInstallService._checkAddlibExtracted(asset.folderName, ExtractedFolderPath)) { - return DownloadResult.filesystemFailure; - } - asset.folderHash = await GetHashOfFolder(ExtractedFolderPath); - if (asset.folderHash === undefined) { - return DownloadResult.filesystemFailure; - } - // Remove it, because we don't install from it - fs.rmSync(ExtractedFolderPath, { recursive: true, force: true }); - assetList[assetSuffix] = asset; - } - // we do this last to make sure we don't write a corrupt json file - fs.writeFileSync(AssetListDLPath, JSON.stringify(assetList)); - return DownloadResult.success; - } - - async DownloadLatestAddressLibs(cancellationToken = new CancellationTokenSource().token): Promise { + public async DownloadLatestAddressLibs(cancellationToken = new CancellationTokenSource().token): Promise { const addressLibDownloadPath = await this._pathResolver.getAddressLibraryDownloadFolder(); const addressLibDLJSONPath = await this._pathResolver.getAddressLibraryDownloadJSON(); - let status = await AddressLibraryInstallService._downloadLatestAddressLibs( + let status = await AddLib.DownloadLatestAddressLibs( addressLibDownloadPath, addressLibDLJSONPath, cancellationToken @@ -267,66 +55,9 @@ export class AddressLibraryInstallService implements IAddressLibraryInstallServi return status; } - private static async _getAssetList(jsonPath: string) { - if (!fs.existsSync(jsonPath)) { - return undefined; - } - const contents = fs.readFileSync(jsonPath, 'utf8'); - if (!contents || contents.length == 0) { - // json is corrupt - return undefined; - } - const assetList = JSON.parse(contents) as AddressLibReleaseAssetList; - if (!assetList) { - // json is corrupt - return undefined; - } - // check integrity - for (const _assetSuffix in AddressLibAssetSuffix) { - const assetSuffix = _assetSuffix as AddressLibAssetSuffix; - const currentAsset = _getAsset(assetList, assetSuffix); - if (!currentAsset) { - // json is corrupt - return undefined; - } - if ( - !currentAsset.zipFile || - !currentAsset.zipFileHash || - !currentAsset.folderName || - !currentAsset.folderHash - ) { - // json is corrupt - return undefined; - } - } - return assetList; - } - - private async getCurrentDownloadedAssetList(): Promise { + private async getCurrentDownloadedAssetList(): Promise { const addressLibDLJSONPath = await this._pathResolver.getAddressLibraryDownloadJSON(); - return AddressLibraryInstallService._getAssetList(addressLibDLJSONPath); - } - - private static async _checkDownloadIntegrity( - downloadpath: string, - assetList: AddressLibReleaseAssetList - ): Promise { - if (!assetList) { - return false; - } - for (const _assetSuffix in AddressLibAssetSuffix) { - const assetSuffix = _assetSuffix as AddressLibAssetSuffix; - const currentAsset = _getAsset(assetList, assetSuffix); - const assetName = currentAsset.zipFile; - const assetPath = path.join(downloadpath, assetName); - if (!fs.existsSync(assetPath)) { - return false; - } - if (!CheckHashFile(assetPath, currentAsset.zipFileHash)) { - return false; - } - } - return true; + return AddLib.GetAssetList(addressLibDLJSONPath); } private async checkDownloadIntegrity(): Promise { @@ -335,7 +66,7 @@ export class AddressLibraryInstallService implements IAddressLibraryInstallServi if (!assetList) { return false; } - return await AddressLibraryInstallService._checkDownloadIntegrity(addressLibDownloadPath, assetList); + return await AddLib._checkDownloadIntegrity(addressLibDownloadPath, assetList); } /** @@ -346,21 +77,21 @@ export class AddressLibraryInstallService implements IAddressLibraryInstallServi * - If the address library is downloaded and the version is up to date, it will return `latest` * @returns AddressLibDownloadedState */ - async getDownloadedState(): Promise { + public async getDownloadedState(): Promise { // If it's not downloaded or the download is corrupt, we return notDownloaded if (!(await this.checkDownloadIntegrity())) { return AddressLibDownloadedState.notDownloaded; } - // At this point, we know if SOME version is downloaded and is valid, but we don't know if it's the latest const assetList = await this.getCurrentDownloadedAssetList(); if (!assetList) { return AddressLibDownloadedState.notDownloaded; } - const latestReleaseInfo = await AddressLibraryInstallService.getLatestAddLibReleaseInfo(); + // At this point, we know if SOME version is downloaded and is valid, but we don't know if it's the latest + const latestReleaseInfo = await AddLib.getLatestAddLibReleaseInfo(); if (!latestReleaseInfo) { return AddressLibDownloadedState.downloadedButCantCheckForUpdates; } - const latestAssetList = AddressLibraryInstallService.getAssetListFromAddLibRelease(latestReleaseInfo); + const latestAssetList = AddLib.getAssetListFromAddLibRelease(latestReleaseInfo); if (!latestAssetList) { return AddressLibDownloadedState.downloadedButCantCheckForUpdates; } @@ -370,129 +101,71 @@ export class AddressLibraryInstallService implements IAddressLibraryInstallServi return AddressLibDownloadedState.latest; } + /** - * @param game The game to check for - * @param modsDir The mods directory to check in - * @param assetList The downloaded Address Library asset list to check against. - * If not provided, we don't check if it's outdated - * @returns AddressLibInstalledState + * Right now, this just checks if the address libraries are installed or not + * It returns either "Installed" or "Not Installed". + * In the future, we might check if the installed address libraries are outdated or not + * @param game + * @param modsDir + * @returns */ - private static async _checkAddressLibsInstalled( - game: PapyrusGame, - modsDir: string, - assetList?: AddressLibReleaseAssetList - ): Promise { - const addressLibFolderNames = getAddressLibNames(game); - for (let _name in addressLibFolderNames) { - const name = _name as AddressLibraryName; - const suffix = getAsssetLibraryDLSuffix(name); - const addressLibInstallPath = path.join(modsDir, name); - if (!await AddressLibraryInstallService._checkAddlibExtracted(name, addressLibInstallPath)) { - return AddressLibInstalledState.notInstalled; - } - if (assetList) { - const asset = _getAsset(assetList, suffix); - if (!asset) { - throw new Error('Asset list is corrupt'); - } - const folderHash = await GetHashOfFolder(addressLibInstallPath); - if (!folderHash) { - return AddressLibInstalledState.notInstalled; - } - if (folderHash != asset.folderHash) { - return AddressLibInstalledState.outdated; - } - } + public async getInstallState(game: PapyrusGame, modsDir?: string): Promise { + const ModsInstallDir = modsDir || (await this._pathResolver.getModParentPath(game)) || ''; + if (!ModsInstallDir || ModsInstallDir.length === 0) { + return AddressLibInstalledState.notInstalled; } - return AddressLibInstalledState.installed; - } - - async getInstallState(game: PapyrusGame, modsDir?: string): Promise { - const ModsInstallDir = modsDir || (await this._pathResolver.getModParentPath(game)); - const state = await AddressLibraryInstallService._checkAddressLibsInstalled(game, modsDir); + const state = await AddLib._checkAddressLibsInstalled(game, ModsInstallDir); if (state === AddressLibInstalledState.notInstalled) { return AddressLibInstalledState.notInstalled; } // At this point, we know if the address libraries are installed or not, but we don't know if they're outdated + // TODO: For right now, we're not going to attempt to update the address libraries if they're outdated + // We will have to have to ensure that the repo that we are using is always up-to-date before we start doing this + return state; + const downloadedState = await this.getDownloadedState(); // We don't check the installed address lib versions if we don't have the latest version downloaded if (downloadedState !== AddressLibDownloadedState.latest) { - return AddressLibInstalledState.installedButCantCheckForUpdates; + return AddressLibInstalledState.installedButCantCheckForUpdates; } const assetList = await this.getCurrentDownloadedAssetList(); if (!assetList) { return AddressLibInstalledState.installedButCantCheckForUpdates; } - return await AddressLibraryInstallService._checkAddressLibsInstalled(game, ModsInstallDir, assetList); - } - /** - * This checks to see if the folder has at least one file in it - * @param name - * @param installpath - the full path to the folder to check, including the address library name - * @returns - */ - private static async _checkAddlibExtracted(name: AddressLibraryName, installpath: string): Promise { - if (!fs.existsSync(installpath)) { - return false; - } - // TODO: refactor this - const SEDIR = name.indexOf('SKSE') >= 0 ? 'SKSE' : 'F4SE'; - const pluginsdir = path.join(installpath, SEDIR, 'Plugins'); - const files = fs.readdirSync(pluginsdir, {withFileTypes: true }); - if (files.length == 0) { - return false; - } - return true; + return await AddLib._checkAddressLibsInstalled(game, ModsInstallDir, assetList); } - async installLibraries( + public async installLibraries( game: PapyrusGame, forceDownload: boolean = false, cancellationToken = new CancellationTokenSource().token, modsDir: string | undefined ): Promise { const ParentInstallDir = modsDir || (await this._pathResolver.getModParentPath(game)); + if (!ParentInstallDir) { + return false; + } const addressLibDownloadPath = await this._pathResolver.getAddressLibraryDownloadFolder(); let downloadedState = await this.getDownloadedState(); if (downloadedState === AddressLibDownloadedState.notDownloaded) { if (forceDownload) { - if ((await this.DownloadLatestAddressLibs(cancellationToken)) != DownloadResult.success) { - return false; - } + if ((await this.DownloadLatestAddressLibs(cancellationToken)) != DownloadResult.success) { + return false; + } } else { - return false; + return false; } } - const assetList = await this.getCurrentDownloadedAssetList(); if (!assetList) { return false; } - const addressLibNames = getAddressLibNames(game); - for (let _name in addressLibNames) { - const name = _name as AddressLibraryName; - if (cancellationToken.isCancellationRequested) { - return false; - } - const suffix = getAsssetLibraryDLSuffix(name); - const asset = _getAsset(assetList, suffix); - if (!asset) { - throw new Error('Asset list is corrupt'); - } - const addressLibInstallPath = path.join(ParentInstallDir, name); - await mkdirIfNeeded(path.dirname(addressLibInstallPath)); - await extractZip(path.join(addressLibDownloadPath, asset.zipFile), { - dir: path.dirname(addressLibInstallPath), - }); - if (!await AddressLibraryInstallService._checkAddlibExtracted(name, addressLibInstallPath)) { - return false; - } - } - return true; + return AddLib._installAddressLibs(game, ParentInstallDir, addressLibDownloadPath, assetList, cancellationToken); } } export const IAddressLibraryInstallService: interfaces.ServiceIdentifier = - Symbol('AddressLibraryInstallService'); + Symbol('addressLibraryInstallService'); diff --git a/src/papyrus-lang-vscode/src/debugger/DebugLauncherService.ts b/src/papyrus-lang-vscode/src/debugger/DebugLauncherService.ts index 7baef535..98e41ed7 100644 --- a/src/papyrus-lang-vscode/src/debugger/DebugLauncherService.ts +++ b/src/papyrus-lang-vscode/src/debugger/DebugLauncherService.ts @@ -2,7 +2,7 @@ import { inject, injectable, interfaces } from 'inversify'; import { IExtensionConfigProvider } from '../ExtensionConfigProvider'; import { CancellationToken, CancellationTokenSource, window } from 'vscode'; import { IPathResolver } from '../common/PathResolver'; -import { PapyrusGame } from "../PapyrusGame"; +import { PapyrusGame } from '../PapyrusGame'; import { ILanguageClientManager } from '../server/LanguageClientManager'; import { getGameIsRunning, getGamePIDs, mkdirIfNeeded } from '../Utilities'; @@ -13,6 +13,8 @@ import { promisify } from 'util'; import md5File from 'md5-file'; import { ChildProcess, spawn } from 'node:child_process'; import { timer } from 'rxjs'; +import { execFile as _execFile } from 'child_process'; +const execFile = promisify(_execFile); const exists = promisify(fs.exists); const copyFile = promisify(fs.copyFile); @@ -22,19 +24,25 @@ export enum DebugLaunchState { success, launcherError, gameFailedToStart, + gameExitedBeforeOpening, multipleGamesRunning, cancelled, } export interface IDebugLauncherService { tearDownAfterDebug(): Promise; runLauncher( - launcherPath: string, - launcherArgs: string[], + launcherCommand: LaunchCommand, game: PapyrusGame, + portToCheck: number, cancellationToken?: CancellationToken ): Promise; } +export interface LaunchCommand { + command: string; + args: string[]; +} + @injectable() export class DebugLauncherService implements IDebugLauncherService { private readonly _configProvider: IExtensionConfigProvider; @@ -57,6 +65,7 @@ export class DebugLauncherService implements IDebugLauncherService { } async tearDownAfterDebug() { + // If MO2 was already opened by the user before launch, the process would have detached and this will be closed anyway if (this.launcherProcess) { this.launcherProcess.removeAllListeners(); this.launcherProcess.kill(); @@ -87,9 +96,9 @@ export class DebugLauncherService implements IDebugLauncherService { } async runLauncher( - launcherPath: string, - launcherArgs: string[], + launcherCommand: LaunchCommand, game: PapyrusGame, + portToCheck: number, cancellationToken: CancellationToken | undefined ): Promise { await this.tearDownAfterDebug(); @@ -98,13 +107,23 @@ export class DebugLauncherService implements IDebugLauncherService { cancellationToken = this.cancellationTokenSource.token; } this.currentGame = game; - this.launcherProcess = spawn(launcherPath, launcherArgs, { - detached: true, - stdio: 'ignore', + let cmd = launcherCommand.command; + let args = launcherCommand.args; + let _stdOut: string = ''; + let _stdErr: string = ''; + this.launcherProcess = spawn(cmd, args); + if (!this.launcherProcess || !this.launcherProcess.stdout || !this.launcherProcess.stderr) { + window.showErrorMessage(`Failed to start launcher process.\ncmd: ${cmd}\nargs: ${args.join(' ')}`); + return DebugLaunchState.launcherError; + } + this.launcherProcess.stdout.on('data', (data) => { + _stdOut += data; }); - + this.launcherProcess.stderr.on('data', (data) => { + _stdErr += data; + }); + const GameStartTimeout = 15000; // get the current system time - const GameStartTimeout = 10000; let startTime = new Date().getTime(); // wait for the games process to start while (!cancellationToken.isCancellationRequested) { @@ -114,6 +133,11 @@ export class DebugLauncherService implements IDebugLauncherService { !this.launcherProcess || (this.launcherProcess.exitCode !== null && this.launcherProcess.exitCode !== 0) ) { + window.showErrorMessage( + `Launcher process exited with error code ${ + this.launcherProcess.exitCode + }.\ncmd: ${cmd}\nargs: ${args.join(' ')}\nstderr: ${_stdErr}\nstdout: ${_stdOut}` + ); return DebugLaunchState.launcherError; } } else { @@ -126,7 +150,7 @@ export class DebugLauncherService implements IDebugLauncherService { return DebugLaunchState.cancelled; } // we can't get the PID of the game from the launcher process because - // both MO2 and the script extender loaders forks and deatches the game process + // both MO2 and the script extender loaders fork and deatch the game process let gamePIDs = await getGamePIDs(game); if (gamePIDs.length === 0) { @@ -138,16 +162,22 @@ export class DebugLauncherService implements IDebugLauncherService { } this.gamePID = gamePIDs[0]; + // TODO: REMOVE THIS SHIT WHEN WE YEET THE DEBUGADAPTERPROXY startTime = new Date().getTime(); // wait for the game to fully load + let waitedForGame = false; while (!cancellationToken.isCancellationRequested) { - if (!(await this.keepSleepingUntil(startTime, GameStartTimeout))) { + if (await this.keepSleepingUntil(startTime, GameStartTimeout)) { + if (!(await getGameIsRunning(game))) { + return DebugLaunchState.gameExitedBeforeOpening; + } + } else { + waitedForGame = true; break; } } - - if (cancellationToken.isCancellationRequested) { + if (!waitedForGame && cancellationToken.isCancellationRequested) { await this.tearDownAfterDebug(); return DebugLaunchState.cancelled; } diff --git a/src/papyrus-lang-vscode/src/debugger/DebugSupportInstallService.ts b/src/papyrus-lang-vscode/src/debugger/DebugSupportInstallService.ts index a621e786..be769b5d 100644 --- a/src/papyrus-lang-vscode/src/debugger/DebugSupportInstallService.ts +++ b/src/papyrus-lang-vscode/src/debugger/DebugSupportInstallService.ts @@ -2,8 +2,8 @@ import { inject, injectable, interfaces } from 'inversify'; import { IExtensionConfigProvider } from '../ExtensionConfigProvider'; import { CancellationToken, CancellationTokenSource } from 'vscode'; import { take } from 'rxjs/operators'; -import { IPathResolver } from '../common/PathResolver'; -import { PapyrusGame } from "../PapyrusGame"; +import { getPluginDllName, IPathResolver, PathResolver } from '../common/PathResolver'; +import { PapyrusGame } from '../PapyrusGame'; import { ILanguageClientManager } from '../server/LanguageClientManager'; import { ClientHostStatus } from '../server/LanguageClientHost'; import { mkdirIfNeeded } from '../Utilities'; @@ -14,7 +14,6 @@ import { promisify } from 'util'; import md5File from 'md5-file'; import { PDSModName } from '../common/constants'; - const exists = promisify(fs.exists); const copyFile = promisify(fs.copyFile); @@ -29,7 +28,7 @@ export enum DebugSupportInstallState { export interface IDebugSupportInstallService { getInstallState(game: PapyrusGame, modsDir?: string): Promise; - installPlugin(game: PapyrusGame, cancellationToken?: CancellationToken, pluginDir?: string): Promise; + installPlugin(game: PapyrusGame, cancellationToken?: CancellationToken, modsDir?: string): Promise; } @injectable() @@ -47,22 +46,29 @@ export class DebugSupportInstallService implements IDebugSupportInstallService { this._configProvider = configProvider; this._pathResolver = pathResolver; } - + private _getMMPluginInstallPath(game: PapyrusGame, modsDir: string ): string { + return path.join( + modsDir, + PDSModName, + PathResolver._getModMgrExtenderPluginRelativePath(game), + getPluginDllName(game, false) + ) + } // TODO: Refactor this properly, right now it's just hacked to work with MO2LaunchDescriptor async getInstallState(game: PapyrusGame, modsDir: string | undefined): Promise { - const config = (await this._configProvider.config.pipe(take(1)).toPromise())[game]; const client = await this._languageClientManager.getLanguageClientHost(game); const status = await client.status.pipe(take(1)).toPromise(); - - if (status === ClientHostStatus.disabled) { - return DebugSupportInstallState.gameDisabled; - } - - if (status === ClientHostStatus.missing) { - return DebugSupportInstallState.gameMissing; + // We bypass these checks if we were given a mods directory, as that means we're in a mod manager. + if (!modsDir) { + if (status === ClientHostStatus.disabled) { + return DebugSupportInstallState.gameDisabled; + } + + if (status === ClientHostStatus.missing) { + return DebugSupportInstallState.gameMissing; + } } - const bundledPluginPath = await this._pathResolver.getDebugPluginBundledPath(game); // If the debugger plugin isn't bundled, we'll assume this is in-development. // TODO: Figure out if this is how it should still be done. Can figure that out once we start doing release @@ -71,7 +77,9 @@ export class DebugSupportInstallService implements IDebugSupportInstallService { return DebugSupportInstallState.installed; } - const installedPluginPath = modsDir ? path.join(modsDir, "Plugins", PDSModName) : await this._pathResolver.getDebugPluginInstallPath(game, false); + const installedPluginPath = modsDir + ? this._getMMPluginInstallPath(game, modsDir) + : await this._pathResolver.getDebugPluginInstallPath(game, false); if (!installedPluginPath || !(await exists(installedPluginPath))) { return DebugSupportInstallState.notInstalled; } @@ -83,7 +91,7 @@ export class DebugSupportInstallService implements IDebugSupportInstallService { return DebugSupportInstallState.incorrectVersion; } - if (config.modDirectoryPath) { + if (config.modDirectoryPath || modsDir) { return DebugSupportInstallState.installedAsMod; } @@ -91,8 +99,14 @@ export class DebugSupportInstallService implements IDebugSupportInstallService { } // TODO: Refactor this properly, right now it's just hacked to work with MO2LaunchDescriptor - async installPlugin(game: PapyrusGame, cancellationToken = new CancellationTokenSource().token, pluginDir: string | undefined): Promise { - const pluginInstallPath = pluginDir || await this._pathResolver.getDebugPluginInstallPath(game, false); + async installPlugin( + game: PapyrusGame, + cancellationToken = new CancellationTokenSource().token, + modsDir: string | undefined + ): Promise { + const pluginInstallPath = modsDir + ? this._getMMPluginInstallPath(game, modsDir) + : await this._pathResolver.getDebugPluginInstallPath(game, false); if (!pluginInstallPath) { return false; } diff --git a/src/papyrus-lang-vscode/src/debugger/GameDebugConfiguratorService.ts b/src/papyrus-lang-vscode/src/debugger/GameDebugConfiguratorService.ts new file mode 100644 index 00000000..d67368a7 --- /dev/null +++ b/src/papyrus-lang-vscode/src/debugger/GameDebugConfiguratorService.ts @@ -0,0 +1,103 @@ +// TODO: Remove, no longer necessary + + + +import { inject, injectable, interfaces } from 'inversify'; +import { IExtensionConfigProvider } from '../ExtensionConfigProvider'; +import { take } from 'rxjs/operators'; +import { IPathResolver } from '../common/PathResolver'; +import { PapyrusGame, getGameIniName } from '../PapyrusGame'; +import { ILanguageClientManager } from '../server/LanguageClientManager'; +import { ClientHostStatus } from '../server/LanguageClientHost'; +import { CheckIfDebuggingIsEnabledInIni, TurnOnDebuggingInIni } from '../common/GameHelpers'; +import { WriteChangesToIni, ParseIniFile } from "../common/INIHelpers"; + +import * as path from 'path'; +import * as fs from 'fs'; +import { promisify } from 'util'; + +const exists = promisify(fs.exists); +const copyFile = promisify(fs.copyFile); +const removeFile = promisify(fs.unlink); + +export enum GameDebugConfigurationState { + debugEnabled, + debugNotEnabled, + gameIniMissing, + gameUserDirMissing, + gameMissing, + gameDisabled +} + +export interface IGameDebugConfiguratorService { + getState(game: PapyrusGame, gameUserDir?: string): Promise; + configureDebug(game: PapyrusGame, gameUserDir?: string): Promise; +} +@injectable() +export class GameDebugConfiguratorService implements IGameDebugConfiguratorService { + private readonly _configProvider: IExtensionConfigProvider; + private readonly _languageClientManager: ILanguageClientManager; + private readonly _pathResolver: IPathResolver; + + constructor( + @inject(ILanguageClientManager) languageClientManager: ILanguageClientManager, + @inject(IExtensionConfigProvider) configProvider: IExtensionConfigProvider, + @inject(IPathResolver) pathResolver: IPathResolver + ) { + this._languageClientManager = languageClientManager; + this._configProvider = configProvider; + this._pathResolver = pathResolver; + } + + async getState(game: PapyrusGame, gameUserDir: string | undefined): Promise { + const client = await this._languageClientManager.getLanguageClientHost(game); + const status = await client.status.pipe(take(1)).toPromise(); + if (!gameUserDir) { + if (status === ClientHostStatus.disabled) { + return GameDebugConfigurationState.gameDisabled; + } + if (status === ClientHostStatus.missing) { + return GameDebugConfigurationState.gameMissing; + } + } + const gameUserDirPath = gameUserDir || await this._pathResolver.getUserGamePath(game); + if (!gameUserDirPath) { + return GameDebugConfigurationState.gameUserDirMissing; + } + const gameIniPath = path.join(gameUserDirPath, getGameIniName(game)); + if (!(await exists(gameIniPath))) { + return GameDebugConfigurationState.gameIniMissing; + } + const inidata = await ParseIniFile(gameIniPath); + if (!inidata) { + return GameDebugConfigurationState.gameIniMissing; + } + if (!CheckIfDebuggingIsEnabledInIni(inidata)) { + return GameDebugConfigurationState.debugNotEnabled; + } + return GameDebugConfigurationState.debugEnabled; + } + + async configureDebug( + game: PapyrusGame, + gameUserDir: string | undefined + ): Promise { + const gameUserDirPath = gameUserDir || await this._pathResolver.getUserGamePath(game); + if (!gameUserDirPath) { + return false; + } + const gameIniPath = path.join(gameUserDirPath, getGameIniName(game)); + if (!(await exists(gameIniPath))) { + return false; + } + const inidata = await ParseIniFile(gameIniPath); + if (!inidata) { + return false; + } + const newinidata = TurnOnDebuggingInIni(inidata); + return await WriteChangesToIni(gameIniPath, newinidata); + } +} + +export const IGameDebugConfiguratorService: interfaces.ServiceIdentifier = + Symbol('GameDebugConfiguratorService'); diff --git a/src/papyrus-lang-vscode/src/debugger/MO2ConfiguratorService.ts b/src/papyrus-lang-vscode/src/debugger/MO2ConfiguratorService.ts new file mode 100644 index 00000000..e0345c54 --- /dev/null +++ b/src/papyrus-lang-vscode/src/debugger/MO2ConfiguratorService.ts @@ -0,0 +1,301 @@ +import { inject, injectable, interfaces } from 'inversify'; +import { IExtensionConfigProvider } from '../ExtensionConfigProvider'; +import { take } from 'rxjs/operators'; +import { IPathResolver } from '../common/PathResolver'; +import { PapyrusGame, getGameIniName } from '../PapyrusGame'; +import { ILanguageClientManager } from '../server/LanguageClientManager'; +import { ClientHostStatus } from '../server/LanguageClientHost'; +import { getPIDsforFullPath, mkdirIfNeeded } from '../Utilities'; + +import * as path from 'path'; +import * as fs from 'fs'; +import { promisify } from 'util'; + +import md5File from 'md5-file'; +import { PDSModName } from '../common/constants'; +import { IDebugSupportInstallService, DebugSupportInstallState } from './DebugSupportInstallService'; +import { IAddressLibraryInstallService, AddressLibInstalledState } from './AddressLibInstallService'; +import { MO2LauncherDescriptor } from './MO2LaunchDescriptorFactory'; +import { CheckIfDebuggingIsEnabledInIni, TurnOnDebuggingInIni } from '../common/GameHelpers'; +import { WriteChangesToIni, ParseIniFile } from '../common/INIHelpers'; +import { + AddRequiredModsToModList, + checkAddressLibrariesExistAndEnabled, + checkPDSModExistsAndEnabled, + isMO2Running, + isOurMO2Running, + killAllMO2Processes, +} from './MO2Helpers'; +import * as MO2Lib from '../common/MO2Lib'; +import { CancellationTokenSource } from 'vscode-languageclient'; +import { CancellationToken } from 'vscode'; +import { execFile as _execFile, spawn } from 'child_process'; +import { mkdir } from 'fs/promises'; +import { AddSeparatorToBeginningOfModList } from '../common/MO2Lib'; +const execFile = promisify(_execFile); +const exists = promisify(fs.exists); +const copyFile = promisify(fs.copyFile); +const removeFile = promisify(fs.unlink); + +export enum MO2LaunchConfigurationStatus { + Ready = 0, + // fixable + PDSNotInstalled = 1 << 0, + PDSIncorrectVersion = 1 << 1, + AddressLibraryNotInstalled = 1 << 2, + AddressLibraryOutdated = 1 << 3, // This is not currently in use + PDSModNotEnabledInModList = 1 << 4, + AddressLibraryModNotEnabledInModList = 1 << 5, + // not fixable + ModListNotParsable = 1 << 6, + UnknownError = 1 << 7, +} + +export interface IMO2ConfiguratorService { + getStateFromConfig(launchDescriptor: MO2LauncherDescriptor): Promise; + fixDebuggerConfiguration( + launchDescriptor: MO2LauncherDescriptor, + cancellationToken?: CancellationToken + ): Promise; +} +function _getErrorMessage(state: MO2LaunchConfigurationStatus) { + switch (state) { + case MO2LaunchConfigurationStatus.Ready: + return 'Ready'; + case MO2LaunchConfigurationStatus.PDSNotInstalled: + return 'Papyrus Debug Support is not installed'; + case MO2LaunchConfigurationStatus.PDSIncorrectVersion: + return 'Papyrus Debug Support is not the correct version'; + case MO2LaunchConfigurationStatus.AddressLibraryNotInstalled: + return 'Address Library is not installed'; + case MO2LaunchConfigurationStatus.AddressLibraryOutdated: // This is not currently in use + return 'Address Library is not the correct version'; + case MO2LaunchConfigurationStatus.PDSModNotEnabledInModList: + return 'Papyrus Debug Support mod is not enabled in the mod list'; + case MO2LaunchConfigurationStatus.AddressLibraryModNotEnabledInModList: + return 'Address Library mod is not enabled in the mod list'; + case MO2LaunchConfigurationStatus.ModListNotParsable: + return 'Mod list is not parsable'; + case MO2LaunchConfigurationStatus.UnknownError: + return 'An unknown error'; + } + return 'An unknown error'; +} + +export function GetErrorMessageFromStatus(state: MO2LaunchConfigurationStatus): string { + let errorMessages = new Array(); + let states = getStates(state); + if (states.length === 1 && states[0] === MO2LaunchConfigurationStatus.Ready) { + return 'Ready'; + } + for (let state of states) { + errorMessages.push(_getErrorMessage(state)); + } + const errMsg = '- ' + errorMessages.join('\n - '); + return errMsg; +} +function getStates(state: MO2LaunchConfigurationStatus): MO2LaunchConfigurationStatus[] { + if (state === MO2LaunchConfigurationStatus.Ready) { + return [MO2LaunchConfigurationStatus.Ready]; + } + let states: MO2LaunchConfigurationStatus[] = []; + let key: keyof typeof MO2LaunchConfigurationStatus; + for (key in MO2LaunchConfigurationStatus) { + let value: MO2LaunchConfigurationStatus = Number(MO2LaunchConfigurationStatus[key]); + if (state & value) { + states.push(value); + } + } + return states; +} + +@injectable() +export class MO2ConfiguratorService implements IMO2ConfiguratorService { + private readonly _configProvider: IExtensionConfigProvider; + private readonly _languageClientManager: ILanguageClientManager; + private readonly _pathResolver: IPathResolver; + private readonly _debugSupportInstallService: IDebugSupportInstallService; + private readonly _addressLibraryInstallService: IAddressLibraryInstallService; + + constructor( + @inject(ILanguageClientManager) languageClientManager: ILanguageClientManager, + @inject(IExtensionConfigProvider) configProvider: IExtensionConfigProvider, + @inject(IPathResolver) pathResolver: IPathResolver, + @inject(IDebugSupportInstallService) debugSupportInstallService: IDebugSupportInstallService, + @inject(IAddressLibraryInstallService) addressLibraryInstallService: IAddressLibraryInstallService + ) { + this._languageClientManager = languageClientManager; + this._configProvider = configProvider; + this._pathResolver = pathResolver; + this._debugSupportInstallService = debugSupportInstallService; + this._addressLibraryInstallService = addressLibraryInstallService; + } + public static errorIsRecoverable(state: MO2LaunchConfigurationStatus): boolean { + if (state === MO2LaunchConfigurationStatus.Ready) { + return true; + } + if ( + state & MO2LaunchConfigurationStatus.ModListNotParsable || + state & MO2LaunchConfigurationStatus.UnknownError + ) { + return false; + } + return true; + } + + public async getStateFromConfig(launchDescriptor: MO2LauncherDescriptor): Promise { + let state = MO2LaunchConfigurationStatus.Ready; + state |= await this.checkPDSisPresent(launchDescriptor); + state |= await this.checkAddressLibsArePresent(launchDescriptor); + state |= await this.checkModListHasPDSEnabled(launchDescriptor); + state |= await this.checkModListHasAddressLibsEnabled(launchDescriptor); + return state; + } + + private async checkPDSisPresent(launchDescriptor: MO2LauncherDescriptor): Promise { + let result = await this._debugSupportInstallService.getInstallState( + launchDescriptor.game, + launchDescriptor.instanceInfo.modsFolder + ); + const ignoreVersion = (await this._configProvider.config.pipe(take(1)).toPromise())[launchDescriptor.game] + .ignoreDebuggerVersion; + if (result !== DebugSupportInstallState.installed && result !== DebugSupportInstallState.installedAsMod) { + if (result === DebugSupportInstallState.incorrectVersion) { + if (ignoreVersion) { + return MO2LaunchConfigurationStatus.Ready; + } + return MO2LaunchConfigurationStatus.PDSIncorrectVersion; + } + // TODO: care about the other states? + return MO2LaunchConfigurationStatus.PDSNotInstalled; + } + return MO2LaunchConfigurationStatus.Ready; + } + + private async checkAddressLibsArePresent( + launchDescriptor: MO2LauncherDescriptor + ): Promise { + let result = await this._addressLibraryInstallService.getInstallState( + launchDescriptor.game, + launchDescriptor.instanceInfo.modsFolder + ); + if (result === AddressLibInstalledState.notInstalled) { + return MO2LaunchConfigurationStatus.AddressLibraryNotInstalled; + } else if (result === AddressLibInstalledState.outdated) { + // not currently in use + return MO2LaunchConfigurationStatus.AddressLibraryOutdated; + } // we don't care about installedButCantCheckForUpdates + return MO2LaunchConfigurationStatus.Ready; + } + + // Check if the MO2 modlist has the PDS mod and the Address Library mod enabled + private async checkModListHasPDSEnabled( + launchDescriptor: MO2LauncherDescriptor + ): Promise { + const modList = await MO2Lib.ParseModListFile(launchDescriptor.profileToLaunchData.modListPath); + // The descriptor factory checked the path and the data was parsable, so this should never happen + if (!modList) { + return MO2LaunchConfigurationStatus.ModListNotParsable; + } + let ret: MO2LaunchConfigurationStatus = MO2LaunchConfigurationStatus.Ready; + if (!checkPDSModExistsAndEnabled(modList)) { + return MO2LaunchConfigurationStatus.PDSModNotEnabledInModList; + } + return MO2LaunchConfigurationStatus.Ready; + } + + private async checkModListHasAddressLibsEnabled( + launchDescriptor: MO2LauncherDescriptor + ): Promise { + const modList = await MO2Lib.ParseModListFile(launchDescriptor.profileToLaunchData.modListPath); + // The descriptor factory checked the path and the data was parsable, so this should never happen + if (!modList) { + return MO2LaunchConfigurationStatus.ModListNotParsable; + } + if (!checkAddressLibrariesExistAndEnabled(modList, launchDescriptor.game)) { + return MO2LaunchConfigurationStatus.AddressLibraryModNotEnabledInModList; + } + + return MO2LaunchConfigurationStatus.Ready; + } + + public async fixDebuggerConfiguration( + launchDescriptor: MO2LauncherDescriptor, + cancellationToken = new CancellationTokenSource().token + ): Promise { + let states = getStates(await this.getStateFromConfig(launchDescriptor)); + for (let state of states) { + switch (state) { + case MO2LaunchConfigurationStatus.Ready: + break; + case MO2LaunchConfigurationStatus.PDSNotInstalled: + case MO2LaunchConfigurationStatus.PDSIncorrectVersion: + if ( + !(await this._debugSupportInstallService.installPlugin( + launchDescriptor.game, + cancellationToken, + launchDescriptor.instanceInfo.modsFolder + )) + ) { + return false; + } + break; + case MO2LaunchConfigurationStatus.AddressLibraryNotInstalled: + case MO2LaunchConfigurationStatus.AddressLibraryOutdated: + if ( + !(await this._addressLibraryInstallService.installLibraries( + launchDescriptor.game, + true, // force download + cancellationToken, + launchDescriptor.instanceInfo.modsFolder + )) + ) { + return false; + } + break; + case MO2LaunchConfigurationStatus.PDSModNotEnabledInModList: + case MO2LaunchConfigurationStatus.AddressLibraryModNotEnabledInModList: + let wasRunning = false; + // if MO2 is running, we have to force a refresh after we add the mods, or it will overwrite our changes + if (await isMO2Running()) { + wasRunning = true; + // if ModOrganizer is currently running, and the installation or selected profile isn't what we're going to run, this will fuck up, kill it + let notOurs = !await isOurMO2Running(launchDescriptor.MO2EXEPath); + if (notOurs || launchDescriptor.instanceInfo.selectedProfile !== launchDescriptor.profileToLaunchData.name){ + await killAllMO2Processes(); + } + } + + const modList = await MO2Lib.ParseModListFile(launchDescriptor.profileToLaunchData.modListPath); + if (!modList) { + return false; + } + + const newmodList = AddRequiredModsToModList(modList, launchDescriptor.game); + if ( + !MO2Lib.WriteChangesToModListFile(launchDescriptor.profileToLaunchData.modListPath, newmodList) + ) { + return false; + } + if (wasRunning) { + spawn(launchDescriptor.MO2EXEPath, ['-p', launchDescriptor.profileToLaunchData.name, "refresh"], { + detached: true, + stdio: 'ignore', + }).unref(); + } + break; + default: + // shouldn't reach here + throw new Error(`Unknown state in fixDebuggerConfiguration`); + } + if (state === MO2LaunchConfigurationStatus.Ready) { + break; + } + } + + return true; + } +} + +export const IMO2ConfiguratorService: interfaces.ServiceIdentifier = + Symbol('mo2ConfiguratorService'); diff --git a/src/papyrus-lang-vscode/src/debugger/MO2Helpers.ts b/src/papyrus-lang-vscode/src/debugger/MO2Helpers.ts index 58202c29..fc5493b7 100644 --- a/src/papyrus-lang-vscode/src/debugger/MO2Helpers.ts +++ b/src/papyrus-lang-vscode/src/debugger/MO2Helpers.ts @@ -1,314 +1,229 @@ -import { existsSync, openSync, readFileSync, writeFileSync } from 'fs'; -import path from 'path'; -import { getRegistryKeyForGame, getScriptExtenderName } from "../PapyrusGame"; -import { PapyrusGame } from "../PapyrusGame"; - - -import * as ini from 'ini'; - +import { exists, existsSync } from 'fs'; +import path, { normalize } from 'path'; import { - AddressLibraryF4SEModName, - AddressLibraryName, - AddressLibrarySKSEAEModName, - AddressLibrarySKSEModName, - PDSModName, -} from '../common/constants'; - -export interface INIData{ - [key: string]: any; -} -export enum ModEnabledState { - unmanaged = '*', - enabled = '+', - disabled = '-', - unknown = '?', -} -export class ModListItem { - public name: string = ''; - public enabled: ModEnabledState = ModEnabledState.unmanaged; - constructor(name: string, enabled: ModEnabledState) { - this.name = name; - this.enabled = enabled; - } -} - - - -/** - * Will return the path to the plugins folder for the given game relative to the game's data folder - * - * - * @param game fallout4 or skyrimse - * @returns - */ -export function getRelativePluginPath(game: PapyrusGame) { - return `${getScriptExtenderName(game)}/Plugins`; -} - -export function getGameIniName(game: PapyrusGame): string { - return game == PapyrusGame.fallout4 ? 'fallout4.ini' :'skyrim.ini'; -} - -export function getGameIniPath(profilePath: string, game: PapyrusGame): string { - let iniName = getGameIniName(game); - return path.join(profilePath, iniName); -} - -// TODO: Refactor these "Find" functions into PathResolver -export function GetGlobalGameSavedDataFolder(game: PapyrusGame){ - if (process.env.HOMEPATH === undefined) { - return undefined; + getExecutableNameForGame, + getGameIniName, + getScriptExtenderExecutableName, + getScriptExtenderName, +} from '../PapyrusGame'; +import { PapyrusGame } from '../PapyrusGame'; +import { PDSModName } from '../common/constants'; +import { DetermineGameVariant, FindUserGamePath, getAddressLibNames } from '../common/GameHelpers'; +import { getEnvFromProcess, getGamePIDs, getPIDforProcessName, getPIDsforFullPath } from '../Utilities'; +import * as MO2Lib from '../common/MO2Lib'; +import { INIData, ParseIniFile } from '../common/INIHelpers'; + +// ours +export const PapyrusMO2Ids: MO2Lib.MO2LongGameID[] = ['Fallout 4', 'Skyrim Special Edition', 'Skyrim']; + +export function GetMO2GameID(game: PapyrusGame): MO2Lib.MO2LongGameID { + switch (game) { + case PapyrusGame.fallout4: + return 'Fallout 4'; + case PapyrusGame.skyrimSpecialEdition: + return 'Skyrim Special Edition'; + case PapyrusGame.skyrim: + return 'Skyrim'; } - return path.join(process.env.HOMEPATH, "Documents", "My Games", getRegistryKeyForGame(game)); } -export function GetLocalAppDataFolder(){ - return process.env.LOCALAPPDATA; -} - -export async function FindMO2InstanceIniPath(modsFolder: string, MO2EXEPath: string, instanceName: string | undefined){ - // check if the instance ini is in the same folder as the mods folder - let instanceIniPath = path.join(path.dirname(modsFolder), 'ModOrganizer.ini'); - if (!existsSync(instanceIniPath)) { - // check if it's a portable instance - let portableSigil = path.join(path.dirname(MO2EXEPath), 'portable.txt'); - // if it's not a portable instance, check appdata - if(!existsSync(portableSigil)){ - if (!instanceName) { - return undefined; - } - let appdata = GetLocalAppDataFolder(); - if (appdata === undefined) { - return undefined; - } - instanceIniPath = path.join(appdata, 'ModOrganizer', instanceName, 'ModOrganizer.ini'); - } else { - if (instanceName === "portable") { - // we shouldn't be here - throw new Error("FUCK!"); - } - // portable instance, instance ini should be in the same dir as the exe - instanceIniPath = path.join(path.dirname(MO2EXEPath), 'ModOrganizer.ini'); - } +export function GetPapyrusGameFromMO2GameID(game: MO2Lib.MO2LongGameID): PapyrusGame | undefined { + switch (game) { + case 'Fallout 4': + return PapyrusGame.fallout4; + case 'Skyrim Special Edition': + return PapyrusGame.skyrimSpecialEdition; + case 'Skyrim': + return PapyrusGame.skyrim; } - if (!existsSync(instanceIniPath)) { - return undefined; - } - - return instanceIniPath; -} - -export async function FindMO2ProfilesFolder(modsFolder: string, MO2InstanceIniData : INIData): Promise { - let parentFolder = path.dirname(modsFolder); - let profilesFolder = path.join(parentFolder, 'profiles'); - if (existsSync(profilesFolder)) { - return profilesFolder; - } - if (!MO2InstanceIniData) { return undefined; - } - // if it's not there, then we have to check the Mod Organizer 2 instance ini file - profilesFolder = MO2InstanceIniData.Settings.profiles_directory; - if (!profilesFolder) { - return undefined; - } - if (profilesFolder.startsWith('%BASE_DIR%')) { - profilesFolder = path.join(parentFolder, profilesFolder.substring(10).trim()); - } else if (!path.isAbsolute(profilesFolder)) { - profilesFolder = path.join(parentFolder, profilesFolder); - } - if (existsSync(profilesFolder)) { - return profilesFolder; - } - return undefined; } -export function getAddressLibNames(game: PapyrusGame): AddressLibraryName[] { - if (game === PapyrusGame.fallout4) { - return [AddressLibraryF4SEModName]; - } else if (game === PapyrusGame.skyrimSpecialEdition) { - return [AddressLibrarySKSEModName, AddressLibrarySKSEAEModName]; - } - throw new Error("ERROR: Unsupported game!"); +export function checkPDSModExistsAndEnabled(modlist: Array) { + return MO2Lib.checkIfModExistsAndEnabled(modlist, PDSModName); } -export async function ParseIniFile(IniPath: string): Promise { - let IniText = readFileSync(IniPath, 'utf-8'); - if (!IniText) { - return undefined; +export function checkAddressLibrariesExistAndEnabled(modlist: Array, game: PapyrusGame) { + const names = getAddressLibNames(game); + for (let name of names) { + if (!MO2Lib.checkIfModExistsAndEnabled(modlist, name)) { + return false; + } } - let IniObject = ini.parse(readFileSync(IniPath, 'utf-8')); - return IniObject as INIData; -} - -export function CheckIfDebuggingIsEnabledInIni(skyrimIni: INIData) { - return ( - skyrimIni.Papyrus.bLoadDebugInformation === 1 && - skyrimIni.Papyrus.bEnableTrace === 1 && - skyrimIni.Papyrus.bEnableLogging === 1 - ); -} - -export function TurnOnDebuggingInIni(skyrimIni: INIData) { - const _ini = skyrimIni; - _ini.Papyrus.bLoadDebugInformation = 1; - _ini.Papyrus.bEnableTrace = 1; - _ini.Papyrus.bEnableLogging = 1; - return _ini; + return true; } - -/** Parse modlist.txt file contents from Mod Organizer 2 - * - * The format is: - * comments are prefixed with '#' - * mod names are the names of their directories in the mods folder - * enabled mods are prefixed with "+" and disabled mods are prefixed with "-" - * Unmanaged mods (e.g. Game DLC) are prefixed with "*" - * The mods are loaded in order. - * Any mods listed earlier will overwrite files in mods listed later. - * - * They appear in the ModOrganizer gui in reverse order (i.e. the last mod in the file is the first mod in the gui) - * - * Example of a modlist.txt file: - * ```none - * # This file was automatically generated by Mod Organizer. - * +Unofficial Skyrim Special Edition Patch - * -SkyUI - * +Immersive Citizens - AI Overhaul - * *DLC: Automatron - * +Immersive Citizens - OCS patch - * -Auto Loot SE - * *DLC: Far Harbor - * *DLC: Contraptions Workshop - * ``` - * This function returns an array of mod names in the order they appear in the file - */ -export function ParseModListText(modListContents: string): ModListItem[] { - let modlist = new Array(); - const modlistLines = modListContents.split('\n'); - for (let line of modlistLines) { - if (line.charAt(0) === '#' || line === '') { - continue; +export function AddRequiredModsToModList(p_modlist: Array, game: PapyrusGame) { + // add the debug adapter to the modlist + let modlist = p_modlist; + const addlibsNeeded = !checkAddressLibrariesExistAndEnabled(modlist, game); + const pdsNeeded = !checkPDSModExistsAndEnabled(modlist); + if (addlibsNeeded || pdsNeeded) { + if (pdsNeeded) { + modlist = MO2Lib.AddOrEnableModInModList(modlist, PDSModName); } - let indic = line.charAt(0); - let modName = line.substring(1); - let modEnabledState: ModEnabledState | undefined = undefined; - switch (indic) { - case '+': - modEnabledState = ModEnabledState.enabled; - break; - case '-': - modEnabledState = ModEnabledState.disabled; - break; - case '*': - modEnabledState = ModEnabledState.unmanaged; - break; + if (addlibsNeeded) { + let addressLibraryMods = getAddressLibNames(game).map( + (d) => new MO2Lib.ModListItem(d, MO2Lib.ModEnabledState.enabled) + ); + modlist = addressLibraryMods.reduce( + (_modlist, mod) => + MO2Lib.AddOrEnableModInModList(_modlist, mod.name), + modlist + ); } - if (modEnabledState === undefined) { - // skip this line - continue; - } - modlist.push(new ModListItem(modName, modEnabledState)); } return modlist; } -export async function ParseModListFile(modlistPath: string): Promise { - // create an ordered map of mod names to their enabled state - const modlistContents = readFileSync(modlistPath, 'utf8'); - if (!modlistContents) { - return undefined; - } - return ParseModListText(modlistContents); +export function GetMO2GameShortIdentifier(game: PapyrusGame): string { + let gamestring = + game === PapyrusGame.skyrimSpecialEdition ? 'skyrimse' : PapyrusGame[game].toLowerCase().replace(/ /g, ''); + return gamestring; } -export async function WriteChangesToIni(gameIniPath: string, skyrimIni: INIData) { - const file = openSync(gameIniPath, 'w'); - if (!file) { +/** + * Checks if the game was launched with MO2 + * + * ModOrganizer launches the game with a modified PATH variable, which is used to load the dlls from the MO2 folder + * We check for the existence of this PATH component and if it points to the MO2 folder + * @param game + * @returns boolean + */ +export async function WasGameLaunchedWithMO2(game: PapyrusGame) { + // get GamePID + const pids = await getGamePIDs(game); + if (!pids || pids.length === 0) { + return false; + } + const pid = pids[0]; + // get env from process + let otherEnv = await getEnvFromProcess(pid); + if (!otherEnv) { + return false; + } + let pathVar: string = otherEnv['Path']; + if (!pathVar) { + return false; + } + let pathVarSplit = pathVar.split(';'); + if (pathVarSplit.length === 0 || !pathVarSplit[0]) { + return false; + } + let firstPath = path.normalize(pathVarSplit[0]); + if (!firstPath) { return false; } - writeFileSync(file, ini.stringify(skyrimIni)); + let basename = path.basename(firstPath); + if (basename.toLowerCase() === 'dlls') { + let parentdir = path.dirname(firstPath); + let MO2EXEPath = path.join(parentdir, MO2Lib.MO2EXEName); + if (existsSync(MO2EXEPath)) { + return true; + } + } return false; } -// parse moshortcut URI -export function parseMoshortcutURI(moshortcutURI: string): { instanceName: string; exeName: string } { - let moshortcutparts = moshortcutURI.replace('moshortcut://', '').split(':'); - let instanceName = moshortcutparts[0] || 'portable'; - let exeName = moshortcutparts[1]; - return { instanceName, exeName }; -} - -export function checkPDSModExistsAndEnabled(modlist: Array) { - return modlist.findIndex((mod) => mod.name === PDSModName && mod.enabled === ModEnabledState.enabled) !== -1; -} +export async function GetPossibleMO2InstancesForModFolder( + modsFolder: string, + game: PapyrusGame +): Promise { + let gameId = GetMO2GameID(game); + let instances = (await MO2Lib.FindAllKnownMO2EXEandInstanceLocations(gameId)).reduce((acc, val) => { + // Combine all the instances together, check to see if the mods folder is in the instance + return acc.concat(val.instances.filter((d) => d.modsFolder === modsFolder)); + }, [] as Array); + if (instances.length === 0) { + return undefined; + } + //filter out the dupes from instances by comparing iniPaths + let filteredInstances = instances.filter((d, i) => { + return instances.findIndex((e) => e.iniPath === d.iniPath) === i; + }); + return filteredInstances; +} + +export async function getGameINIFromMO2Profile( + game: PapyrusGame, + gamePath: string, + profileFolder: string +): Promise { + // Game ini paths for MO2 are different depending on whether the profile has local settings or not + // if [General] LocalSettings=false, then the game ini is in the global game save folder + // if [General] LocalSettings=true, then the game ini is in the profile folder + const settingsIniData = await getMO2ProfileSettingsData(profileFolder); + if (!settingsIniData) { + throw new Error(`Could not get settings ini data`); + } + const gameIniName = getGameIniName(game); + let gameIniPath: string; + if (settingsIniData.General.LocalSettings === false) { + // We don't have local game ini settings, so we need to use the global ones + const variant = await DetermineGameVariant(game, gamePath); + const gameSaveDir = await FindUserGamePath(game, variant); + if (!gameSaveDir || !existsSync(gameSaveDir)) { + throw new Error( + `MO2 profile does not have local game INI settings, but could not find the global game save directory at ${gameSaveDir} (Try running the game once to generate the ini file)` + ); + } + gameIniPath = path.join(gameSaveDir, gameIniName); + if (!existsSync(gameIniPath)) { + throw new Error( + `MO2 profile does not have local game INI settings, but could not find the global game ${game} ini @ ${gameIniPath} (Try running the game once to generate the ini file)` + ); + } + } else { + gameIniPath = path.join(profileFolder, gameIniName); + // TODO: This is fixable by running `ModOrganizer.exe refresh` + if (!existsSync(gameIniPath)) { + throw new Error( + `MO2 profile has local game INI settings, but could not find the local ${game} ini @ ${gameIniPath}` + ); + } + } -export function checkAddressLibraryExistsAndEnabled(modlist: Array, game: PapyrusGame) { - if (game === PapyrusGame.skyrimSpecialEdition) { - //TODO: check for the current version of skyrim SE' - // Right now, we just ensure both versions are installed - return ( - modlist.findIndex( - (mod) => mod.name === AddressLibrarySKSEModName && mod.enabled === ModEnabledState.enabled - ) !== -1 && - modlist.findIndex( - (mod) => mod.name === AddressLibrarySKSEAEModName && mod.enabled === ModEnabledState.enabled - ) !== -1 - ); - } else if (game === PapyrusGame.fallout4) { - return ( - modlist.findIndex( - (mod) => mod.name === AddressLibraryF4SEModName && mod.enabled === ModEnabledState.enabled - ) !== -1 - ); + // We don't save this here, we just use it to check if the game ini is parsable + const gameIniData = await ParseIniFile(gameIniPath); + if (!gameIniData) { + throw new Error(`Game ini file is not parsable, try re-running the game to re-generate the ini file`); } - return modlist.findIndex((mod) => mod.name === PDSModName && mod.enabled === ModEnabledState.enabled) !== -1; + return gameIniPath; } -export function AddModToBeginningOfModList(p_modlist: Array, mod: ModListItem) { - let modlist = p_modlist; - // check if the mod is already in the modlist - let modIndex = modlist.findIndex( - (m) => m.name === mod.name - ); - if (modIndex !== -1) { - // if the mod is already in the modlist, remove it - modlist = modlist.splice(modIndex, 1); - } - - modlist = [mod].concat(modlist); - return modlist; +export async function getMO2ProfileSettingsData(settingsIniPath: string): Promise { + const settingsIniData = await ParseIniFile(settingsIniPath); + if ( + !settingsIniData || + settingsIniData.General === undefined || + settingsIniData.General.LocalSettings === undefined + ) { + throw new Error(`MO2 profile Settings ini file ${settingsIniPath} is not parsable`); + } + return settingsIniData; } -export function AddRequiredModsToModList(p_modlist: Array, game: PapyrusGame) { - // add the debug adapter to the modlist - let modlist = p_modlist; - let debugAdapterMod = new ModListItem(PDSModName, ModEnabledState.enabled); - - let addressLibraryMods = getAddressLibNames(game).map(d => new ModListItem(d, ModEnabledState.enabled)); - - // ensure address libs load before debug adapter by putting them after the debug adapter in the modlist - modlist = AddModToBeginningOfModList(modlist, debugAdapterMod); - modlist = addressLibraryMods.reduce((modlist, mod) => - AddModToBeginningOfModList(modlist, mod), modlist - ); - return modlist; +export async function isMO2Running() { + return (await getPIDforProcessName(MO2Lib.MO2EXEName)).length > 0; } - -export function ModListToText(modlist: Array) { - let modlistText = '# This file was automatically generated by Mod Organizer.'; - for (let mod of modlist) { - modlistText += mod.enabled + mod.name + '\n'; +export async function isMO2ButNotThisOneRunning(MO2EXEPath: string){ + const pids = await getPIDforProcessName(MO2Lib.MO2EXEName); + if (pids.length === 0) { + return false; } - return modlistText; + const ourPids = await getPIDsforFullPath(MO2EXEPath); + if (ourPids.length === 0 ) { + return true; + } + return pids.some((pid) => ourPids.indexOf(pid) === -1); +} +export async function isOurMO2Running(MO2EXEPath: string) { + return (await getPIDsforFullPath(MO2EXEPath)).length > 0; } -export function WriteChangesToModListFile(modlistPath: string, modlist: Array) { - let modlistContents = ModListToText(modlist); - if (!openSync(modlistPath, 'w')) { - return false; +export async function killAllMO2Processes() { + const pids = await getPIDforProcessName(MO2Lib.MO2EXEName); + if (pids.length > 0) { + pids.map((pid) => process.kill(pid)); } - writeFileSync(modlistPath, modlistContents); - return true; } diff --git a/src/papyrus-lang-vscode/src/debugger/MO2LaunchDescriptorFactory.ts b/src/papyrus-lang-vscode/src/debugger/MO2LaunchDescriptorFactory.ts index 194d55b1..5135b955 100644 --- a/src/papyrus-lang-vscode/src/debugger/MO2LaunchDescriptorFactory.ts +++ b/src/papyrus-lang-vscode/src/debugger/MO2LaunchDescriptorFactory.ts @@ -1,42 +1,12 @@ -import { existsSync, fstat, openSync, readFileSync, writeFileSync } from 'fs'; -import * as ini from 'ini'; -import { PapyrusGame } from "../PapyrusGame"; +import { existsSync, statSync } from 'fs'; +import { PapyrusGame } from '../PapyrusGame'; import { MO2Config } from './PapyrusDebugSession'; -import { PDSModName } from '../common/constants'; -import { inject, injectable } from 'inversify'; +import { injectable, interfaces } from 'inversify'; import path from 'path'; -import { - getGameIniPath, - FindMO2ProfilesFolder, - ParseIniFile, - CheckIfDebuggingIsEnabledInIni, - TurnOnDebuggingInIni, - WriteChangesToIni, - ParseModListFile, - checkPDSModExistsAndEnabled, - checkAddressLibraryExistsAndEnabled, - parseMoshortcutURI, - AddRequiredModsToModList, - WriteChangesToModListFile, - ModListItem, - INIData, - FindMO2InstanceIniPath, - GetGlobalGameSavedDataFolder, - getRelativePluginPath, - getGameIniName, -} from './MO2Helpers'; -import { IDebugSupportInstallService, DebugSupportInstallState } from './DebugSupportInstallService'; - -export enum MO2LaunchConfigurationState { - Ready = 0, - PDSNotInstalled = 1 >> 0, - PDSIncorrectVersion = 1 >> 1, - AddressLibraryNotInstalled = 1 >> 2, - GameIniNotSetupForDebugging = 1 >> 4, - PDSModNotEnabledInModList = 1 >> 5, - AddressLibraryModNotEnabledInModList = 1 >> 6, -} +import { GetPapyrusGameFromMO2GameID } from './MO2Helpers'; +import * as MO2Lib from '../common/MO2Lib'; +import { LaunchCommand } from './DebugLauncherService'; export interface MO2ProfileData { name: string; @@ -47,14 +17,12 @@ export interface MO2ProfileData { * @type {string} */ settingsIniPath: string; - settingsIniData: INIData; /** * Path to the txt file that contains the mod list for this profile * Should always be present in profile folder and should always be named "modlist.txt" * @type {string} */ modListPath: string; - modListData: ModListItem[]; /** * Path to the ini file that contains the Skyrim or Fallout 4 settings. * Depending if the profile has local settings, this is either present in the profile folder or in the global save game folder. @@ -62,418 +30,210 @@ export interface MO2ProfileData { * @type {string} */ gameIniPath: string; - gameIniData: INIData; } -export interface MO2InstanceData { - name: string; - modsFolder: string; - profilesFolder: string; - selectedProfileData: MO2ProfileData; -} -export function GetErrorMessageFromState(state: MO2LaunchConfigurationState): string { - let errorMessages = new Array(); - switch (state) { - case MO2LaunchConfigurationState.Ready: - return 'Ready'; - case state & MO2LaunchConfigurationState.PDSNotInstalled: - errorMessages.push('Papyrus Debug Support is not installed'); - case MO2LaunchConfigurationState.PDSIncorrectVersion: - errorMessages.push('Papyrus Debug Support is not the correct version'); - case MO2LaunchConfigurationState.AddressLibraryNotInstalled: - errorMessages.push('Address Library is not installed'); - case MO2LaunchConfigurationState.GameIniNotSetupForDebugging: - errorMessages.push('Game ini is not setup for debugging'); - case MO2LaunchConfigurationState.PDSModNotEnabledInModList: - errorMessages.push('Papyrus Debug Support mod is not enabled in the mod list'); - case MO2LaunchConfigurationState.AddressLibraryModNotEnabledInModList: - errorMessages.push('Address Library mod is not enabled in the mod list'); - default: - } - const errMsg = 'The following errors were found: \n -' + errorMessages.join('\n -'); - return errMsg; + +export interface IMO2LaunchDescriptorFactory { + createMO2LaunchDecriptor( + launcherPath: string, + launcherArgs: string[], + mo2Config: MO2Config, + game: PapyrusGame + ): Promise; } @injectable() -export class MO2LaunchConfigurationFactory { - private readonly _debugSupportInstallService: IDebugSupportInstallService; - - constructor(@inject(IDebugSupportInstallService) debugSupportInstallService: IDebugSupportInstallService) { - this._debugSupportInstallService = debugSupportInstallService; - } +export class MO2LaunchDescriptorFactory implements IMO2LaunchDescriptorFactory { + constructor() {} // TODO: After testing, make these private - public static async populateMO2ProfileData( - name: string, - profileFolder: string, - game: PapyrusGame - ): Promise { + public static async populateMO2ProfileData(name: string, profileFolder: string): Promise { if (!existsSync(profileFolder)) { - throw new Error(`Could not find the profile folder ${profileFolder}}`); + throw new Error(`Invalid MO2 profile: Could not find the profile folder ${profileFolder}}`); } - - // settings.ini should always be present in profiles + // This is the path to the ini file that contains the settings for this profile + // This should always be present; if it isn't, then something is wrong with the profile const settingsIniPath = path.join(profileFolder, 'settings.ini'); if (!existsSync(settingsIniPath)) { - throw new Error(`Could not find the settings.ini file in ${profileFolder}}`); - } - const settingsIniData = await ParseIniFile(settingsIniPath); - if ( - !settingsIniData || - settingsIniData.General === undefined || - settingsIniData.General.LocalSettings === undefined - ) { - throw new Error(`MO2 profile Settings ini file ${settingsIniPath} is not parsable`); - } - - // Game ini paths for MO2 are different depending on whether the profile has local settings or not - // if [General] LocalSettings=false, then the game ini is in the global game save folder - // if [General] LocalSettings=true, then the game ini is in the profile folder - const gameIniName = getGameIniName(game); - let gameIniPath: string; - if (settingsIniData.General.LocalSettings === false) { - // We don't have local game ini settings, so we need to use the global ones - let gameSaveDir = GetGlobalGameSavedDataFolder(game); - if (!gameSaveDir) { - throw new Error(`Could not find the Global ${game} save directory`); - } - if (!existsSync(gameSaveDir)) { - throw new Error( - `MO2 profile does not have local game INI settings, but could not find the global game save directory at ${gameSaveDir}` - ); - } - gameIniPath = path.join(gameSaveDir, gameIniName); - if (!existsSync(gameIniPath)) { - throw new Error( - `MO2 profile does not have local game INI settings, but could not find the global game ${game} ini @ ${gameIniPath} (Try running the game once to generate the ini file)` - ); - } - } else { - gameIniPath = getGameIniPath(profileFolder, game); - if (!existsSync(gameIniPath)) { - throw new Error( - `MO2 profile has local game INI settings, but could not find the local ${game} ini @ ${gameIniPath}` - ); - } - } - - if (!existsSync(gameIniPath)) { - throw new Error(`Could not find the skyrim.ini file @ ${gameIniPath}`); - } - const gameIniData = await ParseIniFile(gameIniPath); - if (!gameIniData) { - throw new Error(`Game ini file is not parsable`); + throw new Error(`Invalid MO2 profile: Could not find the settings.ini file in ${profileFolder}}`); } const ModsListPath = path.join(profileFolder, 'modlist.txt'); if (!existsSync(ModsListPath)) { - throw new Error(`Could not find the modlist.txt file`); + throw new Error(`Invalid MO2 profile: Could not find the modlist.txt file`); } - const ModsListData = await ParseModListFile(ModsListPath); + const ModsListData = await MO2Lib.ParseModListFile(ModsListPath); if (!ModsListData) { - throw new Error(`Mod list file is not parsable`); + throw new Error(`Invalid MO2 profile: Mod list file is not parsable`); } return { name: name, folderPath: profileFolder, settingsIniPath: settingsIniPath, - settingsIniData: settingsIniData, modListPath: ModsListPath, - modListData: ModsListData, - gameIniPath: gameIniPath, - gameIniData: gameIniData, } as MO2ProfileData; } - public static async populateMO2InstanceData(mo2Config: MO2Config, game: PapyrusGame): Promise { - // taken care of by debug config provider - const modsFolder = mo2Config.modsFolder; - if (!mo2Config.modsFolder) { - throw new Error(`Mod directory path is not set`); + public static async PopulateMO2InstanceData( + MO2EXEPath: string, + instanceName: string, + exeTitle: string, + game: PapyrusGame, + instanceINIPath?: string + ) { + let InstanceInfo: MO2Lib.MO2InstanceInfo | undefined; + if (!instanceINIPath) { + InstanceInfo = await MO2Lib.FindInstanceForEXE(MO2EXEPath, instanceName); + } else { + InstanceInfo = await MO2Lib.GetMO2InstanceInfo(instanceINIPath); } - // TODO: Have the debug config provider check this before we get here - const { instanceName, exeName } = parseMoshortcutURI(mo2Config.shortcut); - if (!instanceName || !exeName) { - throw new Error(`Could not parse the shortcut URI ${mo2Config.shortcut}}`); + if (!InstanceInfo) { + throw new Error(`Could not find the instance '${instanceName}' for the MO2 installation at ${MO2EXEPath}`); } - - let profilesFolder = mo2Config.profilesFolder; - // TODO: Consider moving this to the DebugConfigProvider - if (!profilesFolder) { - // Try the parent folder of the mods folder - profilesFolder = path.join(mo2Config.modsFolder, '..', 'profiles'); - // If it's not there, then we have to parse the MO2 ini to find the profiles folder - if (!existsSync(profilesFolder)) { - // Instance directories are always where ModOrganizer.ini is located - const InstanceIniPath = await FindMO2InstanceIniPath( - mo2Config.modsFolder, - mo2Config.MO2EXEPath, - mo2Config.profile - ); - if (!InstanceIniPath || !existsSync(InstanceIniPath)) { - throw new Error(`Profiles Folder not set, but could not find the instance.ini file`); - } - - const InstanceDirectory = path.dirname(InstanceIniPath); - - const InstanceIniData = await ParseIniFile(InstanceIniPath); - if (!InstanceIniData) { - throw new Error( - `Profiles Folder not set, but instance ini file at ${InstanceIniPath} is not parsable` - ); - } - profilesFolder = await FindMO2ProfilesFolder(mo2Config.modsFolder, InstanceIniData); - if (!profilesFolder) { - throw new Error( - `Profiles Folder not set, but could not find the "profiles" folder in the instance directory ${InstanceDirectory}` - ); - } - } + let papgame = GetPapyrusGameFromMO2GameID(InstanceInfo.gameName); + if (!papgame || papgame !== game) { + throw new Error(`Instance ${instanceName} is not for game ${game}`); } - if (existsSync(profilesFolder)) { - throw new Error(`Could not find the "profiles" folder: ${profilesFolder}`); + if (InstanceInfo.gameDirPath === undefined) { + throw new Error(`Instance ${instanceName} does not have a game directory path`); } - - // taken care of by debug config provider - const profileName = mo2Config.profile; - if (!profileName) { - throw new Error(`Profile name is not set`); + if (!existsSync(InstanceInfo.profilesFolder) || !statSync(InstanceInfo.profilesFolder).isDirectory()) { + throw new Error(`Could not find the profiles folder for instance ${instanceName}`); } - - const profileFolder = path.join(profilesFolder, profileName); - if (!existsSync(profileFolder)) { - throw new Error(`Could not find profile folder ${profileName} in ${profilesFolder}`); + if (!existsSync(InstanceInfo.modsFolder) || !statSync(InstanceInfo.modsFolder).isDirectory()) { + throw new Error(`Could not find the mods folder for instance ${instanceName}`); } - - let selectedProfileData: MO2ProfileData; - try { - selectedProfileData = await this.populateMO2ProfileData(profileName, profileFolder, game); - } catch (error) { - throw new Error(`Could not populate the profile data: ${error}`); + if (!InstanceInfo.customExecutables.filter((entry) => entry.title === exeTitle).length) { + throw new Error(`Instance ${instanceName} does not have an executable named ${exeTitle}`); } - - return { - name: instanceName, - modsFolder: modsFolder, - profilesFolder: profilesFolder, - selectedProfileData: selectedProfileData, - } as MO2InstanceData; + return InstanceInfo; } public static async populateMO2LaunchConfiguration( + launcherPath: string, + launcherArgs: string[], mo2Config: MO2Config, game: PapyrusGame ): Promise { // taken care of by debug config provider - if (!mo2Config.modsFolder) { - throw new Error(`Mod directory path is not set`); - } - if (!existsSync(mo2Config.modsFolder)) { - throw new Error(`Mod directory path does not exist`); - } - - // TODO: make the debug config provider do this - if (!mo2Config.profile) { - throw new Error(`Profile is not set`); - } - - let { instanceName, exeName } = parseMoshortcutURI(mo2Config.shortcut); + let { instanceName, exeName } = MO2Lib.parseMoshortcutURI(mo2Config.shortcutURI); if (!instanceName || !exeName) { throw new Error(`Could not parse the shortcut URI`); } - let MO2EXEPath = mo2Config.MO2EXEPath; - if (!MO2EXEPath || !existsSync(MO2EXEPath)) { + let MO2EXEPath = launcherPath; + if (!MO2EXEPath || !existsSync(MO2EXEPath) || !statSync(MO2EXEPath).isFile()) { throw new Error(`Could not find the Mod Organizer 2 executable path`); } - let instanceData: MO2InstanceData; + let instanceData: MO2Lib.MO2InstanceInfo; try { - instanceData = await this.populateMO2InstanceData(mo2Config, game); + instanceData = await this.PopulateMO2InstanceData( + MO2EXEPath, + instanceName, + exeName, + game, + mo2Config.instanceIniPath + ); } catch (error) { throw new Error(`Could not populate the instance data: ${error}`); } - const args = mo2Config.args || []; + const profile = mo2Config.profile || instanceData.selectedProfile; + if (!profile) { + throw new Error(`Could not find a profile to launch`); + } + // check if the instance is + const profilePath = path.join(instanceData.profilesFolder, profile); + // check if it exists and is directory + if (!existsSync(profilePath) || !statSync(profilePath).isDirectory()) { + throw new Error(`Could not find the profile '${profile}' in ${instanceData.profilesFolder}`); + } + const profileData: MO2ProfileData = await this.populateMO2ProfileData(profile, profilePath); + const additionalArgs = launcherArgs; return { - exeName, + exeTitle: exeName, MO2EXEPath, - args, + additionalArgs, game, - instanceData, + instanceInfo: instanceData, + profileToLaunchData: profileData, } as IMO2LauncherDescriptor; } - public async createMO2LaunchDecriptor(mo2Config: MO2Config, game: PapyrusGame): Promise { + public async createMO2LaunchDecriptor( + launcherPath: string, + launcherArgs: string[], + mo2Config: MO2Config, + game: PapyrusGame + ): Promise { + if (!path.isAbsolute(launcherPath)) { + throw new Error(`The launcher path must be an absolute path`); + } let idescriptor: IMO2LauncherDescriptor; try { - idescriptor = await MO2LaunchConfigurationFactory.populateMO2LaunchConfiguration(mo2Config, game); + idescriptor = await MO2LaunchDescriptorFactory.populateMO2LaunchConfiguration( + launcherPath, + launcherArgs, + mo2Config, + game + ); } catch (error) { throw new Error(`Could not create the launch configuration: ${error}`); } - return new MO2LaunchDescriptor( - idescriptor.exeName, - idescriptor.MO2EXEPath, - idescriptor.args, - idescriptor.game, - idescriptor.instanceData, - this._debugSupportInstallService - ); + return new MO2LauncherDescriptor(idescriptor); } -} - -export interface LaunchCommand { - command: string; - args: string[]; + dispose() {} } export interface IMO2LauncherDescriptor { - exeName: string; + exeTitle: string; MO2EXEPath: string; - args: string[]; + additionalArgs: string[]; game: PapyrusGame; - instanceData: MO2InstanceData; - checkIfDebuggerConfigured(): Promise; - fixDebuggerConfiguration(): Promise; + instanceInfo: MO2Lib.MO2InstanceInfo; + profileToLaunchData: MO2ProfileData; getLaunchCommand(): LaunchCommand; } -export class MO2LaunchDescriptor implements IMO2LauncherDescriptor { - public readonly exeName: string; - public readonly MO2EXEPath: string; - public readonly args: string[]; - public readonly game: PapyrusGame; - public readonly instanceData: MO2InstanceData; - - // TODO: Refactor this to not use this - private readonly _debugSupportInstallService: IDebugSupportInstallService; - - constructor( - exeName: string, - MO2EXEPath: string, - args: string[], - game: PapyrusGame, - instanceData: MO2InstanceData, - debugSupportInstallService: IDebugSupportInstallService - ) { - this.exeName = exeName; - this.MO2EXEPath = MO2EXEPath; - this.args = args; - this.game = game; - this.instanceData = instanceData; - this._debugSupportInstallService = debugSupportInstallService; +function joinArgs(args: string[]): string { + let _args = args; + for (let arg in args) { + if (_args[arg].includes(' ') && !_args[arg].startsWith('"') && !_args[arg].endsWith('"')) { + _args[arg] = `"${_args[arg]}"`; + } } - - public async checkIfDebuggerConfigured(): Promise { - let ret: MO2LaunchConfigurationState = - (await this.checkIfModsArePresent()) | - (await this.checkIfGameIniIsCorrectlyConfigured()) | - (await this.checkIfMO2IsCorrectlyConfigured()); - return ret; + return _args.join(' '); +} +export class MO2LauncherDescriptor implements IMO2LauncherDescriptor { + public readonly exeTitle: string = ''; + public readonly MO2EXEPath: string = ''; + public readonly additionalArgs: string[] = []; + public readonly game: PapyrusGame = PapyrusGame.skyrim; + public readonly instanceInfo: MO2Lib.MO2InstanceInfo = {} as MO2Lib.MO2InstanceInfo; + public readonly profileToLaunchData: MO2ProfileData = {} as MO2ProfileData; + + constructor(idecriptor: IMO2LauncherDescriptor) { + this.exeTitle = idecriptor.exeTitle; + this.MO2EXEPath = idecriptor.MO2EXEPath; + this.additionalArgs = idecriptor.additionalArgs; + this.game = idecriptor.game; + this.instanceInfo = idecriptor.instanceInfo; + this.profileToLaunchData = idecriptor.profileToLaunchData; } - public async fixDebuggerConfiguration(): Promise { - let state = await this.checkIfDebuggerConfigured(); - while (state !== MO2LaunchConfigurationState.Ready) { - switch (state) { - case MO2LaunchConfigurationState.Ready: - break; - case state & MO2LaunchConfigurationState.PDSNotInstalled: - case state & MO2LaunchConfigurationState.PDSIncorrectVersion: - let relativePluginPath = getRelativePluginPath(this.game); - if ( - !(await this._debugSupportInstallService.installPlugin( - this.game, - undefined, - path.join(this.instanceData.modsFolder, PDSModName, relativePluginPath) - )) - ) { - return false; - } - state &= ~MO2LaunchConfigurationState.PDSNotInstalled; - state &= ~MO2LaunchConfigurationState.PDSIncorrectVersion; - break; - case state & MO2LaunchConfigurationState.GameIniNotSetupForDebugging: - const inidata = await ParseIniFile(this.instanceData.selectedProfileData.gameIniPath); - if (!inidata) { - return false; - } - const newGameIni = TurnOnDebuggingInIni(inidata); - if (!WriteChangesToIni(this.instanceData.selectedProfileData.gameIniPath, newGameIni)) { - return false; - } - this.instanceData.selectedProfileData.gameIniData = newGameIni; - state &= ~MO2LaunchConfigurationState.GameIniNotSetupForDebugging; - break; - case state & MO2LaunchConfigurationState.PDSModNotEnabledInModList: - case state & MO2LaunchConfigurationState.AddressLibraryModNotEnabledInModList: - const modList = await ParseModListFile(this.instanceData.selectedProfileData.modListPath); - if (!modList) { - return false; - } - const newmodList = AddRequiredModsToModList(modList, this.game); - if (!WriteChangesToModListFile(this.instanceData.selectedProfileData.modListPath, modList)) { - return false; - } - this.instanceData.selectedProfileData.modListData = newmodList; - state &= ~MO2LaunchConfigurationState.PDSModNotEnabledInModList; - state &= ~MO2LaunchConfigurationState.AddressLibraryModNotEnabledInModList; - break; - default: - // shouldn't reach here - throw new Error(`Unknown state in fixDebuggerConfiguration`); - } - if (state === MO2LaunchConfigurationState.Ready) { - break; - } - } - return true; - } public getLaunchCommand(): LaunchCommand { let command = this.MO2EXEPath; - let cmdargs = ['-p', this.instanceData.selectedProfileData.name]; - if (this.instanceData.name !== 'portable') { - cmdargs = cmdargs.concat(['-i', this.instanceData.name]); + let cmdargs = ['-p', this.profileToLaunchData.name]; + if (this.instanceInfo.name !== 'portable') { + cmdargs = cmdargs.concat(['-i', this.instanceInfo.name]); } - cmdargs.concat('-e', this.exeName); - if (this.args) { - cmdargs = cmdargs.concat(['-a'].concat(this.args)); + cmdargs = cmdargs.concat('run', '-e', this.exeTitle); + if (this.additionalArgs.length > 0) { + cmdargs = cmdargs.concat(['-a', joinArgs(this.additionalArgs)]); } return { command: command, args: cmdargs, } as LaunchCommand; } - - public async checkIfModsArePresent(): Promise { - // TODO: Change this to not have to read global state - let result = await this._debugSupportInstallService.getInstallState(this.game, this.instanceData.modsFolder); - if (result !== DebugSupportInstallState.installed) { - if (result === DebugSupportInstallState.notInstalled) { - return MO2LaunchConfigurationState.PDSNotInstalled; - } - if (result === DebugSupportInstallState.incorrectVersion) { - return MO2LaunchConfigurationState.PDSIncorrectVersion; - } else { - // TODO : FIX THIS - throw new Error(`Unknown result from getInstallState`); - } - } - return MO2LaunchConfigurationState.Ready; - } - - async checkIfGameIniIsCorrectlyConfigured(): Promise { - if (!CheckIfDebuggingIsEnabledInIni(this.instanceData.selectedProfileData.gameIniData)) { - return MO2LaunchConfigurationState.GameIniNotSetupForDebugging; - } - return MO2LaunchConfigurationState.Ready; - } - - // Check if the MO2 modlist has the PDS mod and the Address Library mod enabled - async checkIfMO2IsCorrectlyConfigured(): Promise { - let ret: MO2LaunchConfigurationState = MO2LaunchConfigurationState.Ready; - if (!checkPDSModExistsAndEnabled(this.instanceData.selectedProfileData.modListData)) { - ret |= MO2LaunchConfigurationState.PDSModNotEnabledInModList; - } - if (!checkAddressLibraryExistsAndEnabled(this.instanceData.selectedProfileData.modListData, this.game)) { - ret |= MO2LaunchConfigurationState.AddressLibraryModNotEnabledInModList; - } - return ret; - } } + +export const IMO2LaunchDescriptorFactory: interfaces.ServiceIdentifier = + Symbol('mo2LaunchDescriptorFactory'); diff --git a/src/papyrus-lang-vscode/src/debugger/PapyrusDebugAdapterDescriptorFactory.ts b/src/papyrus-lang-vscode/src/debugger/PapyrusDebugAdapterDescriptorFactory.ts index 832fe4f9..9d5854d1 100644 --- a/src/papyrus-lang-vscode/src/debugger/PapyrusDebugAdapterDescriptorFactory.ts +++ b/src/papyrus-lang-vscode/src/debugger/PapyrusDebugAdapterDescriptorFactory.ts @@ -9,8 +9,6 @@ import { Uri, env, CancellationTokenSource, - MessageOptions, - MessageItem } from 'vscode'; import { PapyrusGame, @@ -29,10 +27,14 @@ import { IDebugSupportInstallService, DebugSupportInstallState } from './DebugSu import { ILanguageClientManager } from '../server/LanguageClientManager'; import { showGameDisabledMessage, showGameMissingMessage } from '../features/commands/InstallDebugSupportCommand'; import { inject, injectable } from 'inversify'; -import { ChildProcess, spawn } from 'child_process'; -import { DebugLaunchState, IDebugLauncherService } from './DebugLauncherService'; -import { CancellationToken } from 'vscode-languageclient'; -import { openSync, readSync, readFileSync } from 'fs'; +import { DebugLaunchState, IDebugLauncherService, LaunchCommand } from './DebugLauncherService'; +import { IMO2LauncherDescriptor, IMO2LaunchDescriptorFactory, MO2LaunchDescriptorFactory } from './MO2LaunchDescriptorFactory'; +import { GetErrorMessageFromStatus, IMO2ConfiguratorService, MO2ConfiguratorService, MO2LaunchConfigurationStatus } from './MO2ConfiguratorService'; +import path from 'path'; +import * as fs from 'fs'; +import { promisify } from 'util'; +import { isMO2ButNotThisOneRunning, isMO2Running, isOurMO2Running, killAllMO2Processes } from './MO2Helpers'; +const exists = promisify(fs.exists); const noopExecutable = new DebugAdapterExecutable('node', ['-e', '""']); @@ -60,6 +62,8 @@ export class PapyrusDebugAdapterDescriptorFactory implements DebugAdapterDescrip private readonly _pathResolver: IPathResolver; private readonly _debugSupportInstaller: IDebugSupportInstallService; private readonly _debugLauncher: IDebugLauncherService; + private readonly _MO2LaunchDescriptorFactory: IMO2LaunchDescriptorFactory; + private readonly _MO2ConfiguratorService: IMO2ConfiguratorService; private readonly _registration: Disposable; constructor( @@ -68,7 +72,9 @@ export class PapyrusDebugAdapterDescriptorFactory implements DebugAdapterDescrip @inject(IExtensionConfigProvider) configProvider: IExtensionConfigProvider, @inject(IPathResolver) pathResolver: IPathResolver, @inject(IDebugSupportInstallService) debugSupportInstaller: IDebugSupportInstallService, - @inject(IDebugLauncherService) debugLauncher: IDebugLauncherService + @inject(IDebugLauncherService) debugLauncher: IDebugLauncherService, + @inject(IMO2LaunchDescriptorFactory) mo2LaunchDescriptorFactory: IMO2LaunchDescriptorFactory, + @inject(IMO2ConfiguratorService) mo2ConfiguratorService: IMO2ConfiguratorService ) { this._languageClientManager = languageClientManager; this._creationKitInfoProvider = creationKitInfoProvider; @@ -76,11 +82,68 @@ export class PapyrusDebugAdapterDescriptorFactory implements DebugAdapterDescrip this._pathResolver = pathResolver; this._debugSupportInstaller = debugSupportInstaller; this._debugLauncher = debugLauncher; + this._MO2LaunchDescriptorFactory = mo2LaunchDescriptorFactory; + this._MO2ConfiguratorService = mo2ConfiguratorService; this._registration = debug.registerDebugAdapterDescriptorFactory('papyrus', this); } - private async ensureGameInstalled(game: PapyrusGame) { - const installState = await this._debugSupportInstaller.getInstallState(game); + private async _ShowAttachDebugSupportInstallMessage(game: PapyrusGame) { + const getExtenderOption = `Get ${getScriptExtenderName(game)}`; + const installOption = `Install ${getScriptExtenderName(game)} Plugin`; + + const selectedInstallOption = await window.showInformationMessage( + `Papyrus debugging support requires a plugin for ${getDisplayNameForGame( + game + )} Script Extender (${getScriptExtenderName( + game + )}) to be installed. After installation has completed, launch ${getShortDisplayNameForGame( + game + )} with ${getScriptExtenderName(game)} and wait until the main menu has loaded.`, + getExtenderOption, + installOption, + 'Cancel' + ); + + switch (selectedInstallOption) { + case installOption: + commands.executeCommand(`papyrus.${game}.installDebuggerSupport`); + break; + case getExtenderOption: + env.openExternal(Uri.parse(getScriptExtenderUrl(game))); + break; + } + return false + } + + private async _ShowLaunchDebugSupportInstallMessage(game: PapyrusGame, launchType: 'MO2' | 'XSE', launcher: IMO2LauncherDescriptor) { + const installOption = `Fix Configuration`; + const state = await this._MO2ConfiguratorService.getStateFromConfig(launcher); + if (state !== MO2LaunchConfigurationStatus.Ready){ + const errorMessage = GetErrorMessageFromStatus(state); + const selectedInstallOption = await window.showInformationMessage( + `The following configuration problems were encountered while attempting to launch ${getDisplayNameForGame(game)}:\n${ + errorMessage + }\nWould you like to fix the configuration?`, + installOption, + 'Cancel' + ); + switch (selectedInstallOption) { + case installOption: + if (launchType === 'MO2') { + commands.executeCommand(`papyrus.${game}.installDebuggerSupport`, [launchType, launcher]); + } else { + commands.executeCommand(`papyrus.${game}.installDebuggerSupport`, [launchType]); + } + break; + case 'Cancel': + return true; + } + } + return false; + } + + private async _attachEnsureGameInstalled(game: PapyrusGame, modsDir?: string) { + const installState = await this._debugSupportInstaller.getInstallState(game, modsDir); switch (installState) { case DebugSupportInstallState.incorrectVersion: { @@ -106,37 +169,11 @@ export class PapyrusDebugAdapterDescriptorFactory implements DebugAdapterDescrip if (selectedUpdateOption === 'Cancel' || selectedUpdateOption === undefined) { return false; } - break; } - case DebugSupportInstallState.notInstalled: { - const getExtenderOption = `Get ${getScriptExtenderName(game)}`; - const installOption = `Install ${getScriptExtenderName(game)} Plugin`; - - const selectedInstallOption = await window.showInformationMessage( - `Papyrus debugging support requires a plugin for ${getDisplayNameForGame( - game - )} Script Extender (${getScriptExtenderName( - game - )}) to be installed. After installation has completed, launch ${getShortDisplayNameForGame( - game - )} with ${getScriptExtenderName(game)} and wait until the main menu has loaded.`, - getExtenderOption, - installOption, - 'Cancel' - ); - - switch (selectedInstallOption) { - case installOption: - commands.executeCommand(`papyrus.${game}.installDebuggerSupport`); - break; - case getExtenderOption: - env.openExternal(Uri.parse(getScriptExtenderUrl(game))); - break; - } - - return false; - } + + case DebugSupportInstallState.notInstalled: + return await this._ShowAttachDebugSupportInstallMessage(game); case DebugSupportInstallState.gameDisabled: showGameDisabledMessage(game); return false; @@ -172,29 +209,48 @@ export class PapyrusDebugAdapterDescriptorFactory implements DebugAdapterDescrip if (game !== PapyrusGame.fallout4 && game !== PapyrusGame.skyrimSpecialEdition) { throw new Error(`'${game}' is not supported by the Papyrus debugger.`); } - if (!await this.ensureGameInstalled(game)){ - session.configuration.noop = true; - return noopExecutable; - } - let launched = DebugLaunchState.success; + let launched = DebugLaunchState.success; + if (session.configuration.request === 'launch'){ + // check if the game is running if (await getGameIsRunning(game)){ throw new Error(`'${getDisplayNameForGame(game)}' is already running. Please close it before launching the debugger.`); } // run the launcher with the args from the configuration // if the launcher is MO2 - let launcherPath: string, launcherArgs: string[] - if(session.configuration.launchType === 'mo2') { - launcherPath = session.configuration.mo2Config?.MO2EXEPath || ""; - const shortcut = session.configuration.mo2Config?.shortcut; - const args = session.configuration.mo2Config?.args || []; - launcherArgs = shortcut ? [shortcut] : []; - if (args) { - launcherArgs = launcherArgs.concat(args); + let launcherPath: string = session.configuration.launcherPath || ""; + if (!launcherPath){ + throw new Error(`'Invalid launch configuration. Launcher path is missing.`); + } + launcherPath = path.normalize(launcherPath); + if (!launcherPath || !await exists(launcherPath)){ + throw new Error(`'Path does not exist!`) + } + let launcherArgs: string[] = session.configuration.args || []; + let LauncherCommand: LaunchCommand; + if(session.configuration.launchType === 'MO2') { + if (session.configuration.mo2Config === undefined){ + throw new Error(`'Invalid launch configuration. MO2 configuration is missing.`); + } + let launcher = await this._MO2LaunchDescriptorFactory.createMO2LaunchDecriptor(launcherPath, launcherArgs, session.configuration.mo2Config, game); + let state = await this._MO2ConfiguratorService.getStateFromConfig(launcher); + if (state !== MO2LaunchConfigurationStatus.Ready) { + if (!await this._ShowLaunchDebugSupportInstallMessage(game, 'MO2', launcher)){ + session.configuration.noop = true; + return noopExecutable; + } } + + // Configuration is ready, get the launch command + LauncherCommand = launcher.getLaunchCommand(); + + // If MO2 is running and the profile is not the one we want to launch, the launch will fuck up, kill it + if (await isMO2ButNotThisOneRunning(launcher.MO2EXEPath) || (launcher.instanceInfo.selectedProfile !== launcher.profileToLaunchData.name)){ + await killAllMO2Processes(); + } + } else if(session.configuration.launchType === 'XSE') { - launcherPath = session.configuration.XSELoaderPath || ""; - launcherArgs = session.configuration.args || []; + LauncherCommand = {command: launcherPath, args: launcherArgs}; } else { // throw an error indicated the launch configuration is invalid throw new Error(`'Invalid launch configuration.`); @@ -202,10 +258,15 @@ export class PapyrusDebugAdapterDescriptorFactory implements DebugAdapterDescrip const cancellationSource = new CancellationTokenSource(); const cancellationToken = cancellationSource.token; - + const port = session.configuration.port || getDefaultPortForGame(game); let wait_message = window.setStatusBarMessage(`Waiting for ${getDisplayNameForGame(game)} to start...`, 30000); - launched = await this._debugLauncher.runLauncher( launcherPath, launcherArgs, game, cancellationToken) + launched = await this._debugLauncher.runLauncher( LauncherCommand, game, port, cancellationToken) wait_message.dispose(); + } else { + if (!await this._attachEnsureGameInstalled(game)){ + session.configuration.noop = true; + return noopExecutable; + } } if (launched != DebugLaunchState.success){ diff --git a/src/papyrus-lang-vscode/src/debugger/PapyrusDebugConfigurationProvider.ts b/src/papyrus-lang-vscode/src/debugger/PapyrusDebugConfigurationProvider.ts index bb46f511..a003d89d 100644 --- a/src/papyrus-lang-vscode/src/debugger/PapyrusDebugConfigurationProvider.ts +++ b/src/papyrus-lang-vscode/src/debugger/PapyrusDebugConfigurationProvider.ts @@ -1,8 +1,19 @@ import { inject, injectable } from 'inversify'; -import { ProviderResult, DebugConfigurationProvider, CancellationToken, WorkspaceFolder, debug, Disposable, DebugConfiguration } from 'vscode'; +import { + ProviderResult, + DebugConfigurationProvider, + CancellationToken, + WorkspaceFolder, + debug, + Disposable, + DebugConfiguration, +} from 'vscode'; import { IPathResolver } from '../common/PathResolver'; -import { PapyrusGame } from "../PapyrusGame"; +import { PapyrusGame } from '../PapyrusGame'; +import { GetPapyrusGameFromMO2GameID } from './MO2Helpers'; +import { FindInstanceForEXE, parseMoshortcutURI } from '../common/MO2Lib'; import { MO2Config, IPapyrusDebugConfiguration } from './PapyrusDebugSession'; +import { getHomeFolder, getLocalAppDataFolder, getTempFolder, getUserName } from '../common/OSHelpers'; // TODO: Auto install F4SE plugin // TODO: Warn if port is not open/if Fallout4.exe is not running @@ -11,42 +22,38 @@ import { MO2Config, IPapyrusDebugConfiguration } from './PapyrusDebugSession'; // TODO: Resolve project from whichever that includes the active editor file. // TODO: Provide configurations based on .ppj files in current directory. - @injectable() export class PapyrusDebugConfigurationProvider implements DebugConfigurationProvider, Disposable { private readonly _registration: Disposable; private readonly _pathResolver: IPathResolver; - constructor( - @inject(IPathResolver) pathResolver: IPathResolver - ) { + constructor(@inject(IPathResolver) pathResolver: IPathResolver) { this._pathResolver = pathResolver; this._registration = debug.registerDebugConfigurationProvider('papyrus', this); } async provideDebugConfigurations( - _folder: WorkspaceFolder | undefined, - _token?: CancellationToken + folder: WorkspaceFolder | undefined, + token?: CancellationToken + // TODO: FIX THIS ): Promise { let PapyrusAttach = { type: 'papyrus', name: 'Fallout 4', game: PapyrusGame.fallout4, request: 'attach', - projectPath: '${workspaceFolder}/${1:Project.ppj}', + projectPath: '${workspaceFolder}/fallout4.ppj', } as IPapyrusDebugConfiguration; let PapyrusMO2Launch = { type: 'papyrus', name: 'Fallout 4 (Launch with MO2)', game: PapyrusGame.fallout4, request: 'launch', - launchType: 'mo2', + launchType: 'MO2', + launcherPath: 'C:/Modding/MO2/ModOrganizer.exe', mo2Config: { - MO2EXEPath: 'C:/Modding/MO2/ModOrganizer.exe', - shortcut: 'moshortcut://Fallout 4:F4SE', - modsFolder: '${env:LOCALAPPDATA}/ModOrganizer/Fallout 4/mods', - args: ['-skipIntro'] - } as MO2Config + shortcutURI: 'moshortcut://Fallout 4:F4SE' + } as MO2Config, } as IPapyrusDebugConfiguration; let PapyruseXSELaunch = { type: 'papyrus', @@ -54,14 +61,10 @@ export class PapyrusDebugConfigurationProvider implements DebugConfigurationProv game: PapyrusGame.fallout4, request: 'launch', launchType: 'XSE', - XSELoaderPath: 'C:/Program Files (x86)/Steam/steamapps/common/Fallout 4/f4se_loader.exe', - args: ['-skipIntro'] + launcherPath: 'C:/Program Files (x86)/Steam/steamapps/common/Fallout 4/f4se_loader.exe', + args: ['-skipIntro'], } as IPapyrusDebugConfiguration; - return [ - PapyrusAttach, - PapyrusMO2Launch, - PapyruseXSELaunch - ]; + return [PapyrusMO2Launch, PapyruseXSELaunch, PapyrusAttach]; } async resolveDebugConfiguration( @@ -69,57 +72,92 @@ export class PapyrusDebugConfigurationProvider implements DebugConfigurationProv debugConfiguration: IPapyrusDebugConfiguration, token?: CancellationToken ): Promise { - if (debugConfiguration.game !== undefined && debugConfiguration.request !== undefined) - { - if (debugConfiguration.request === 'launch') - { - if (debugConfiguration.launchType === 'mo2') - { - if (debugConfiguration.mo2Config !== undefined && debugConfiguration.mo2Config.modsFolder !== undefined && debugConfiguration.mo2Config.MO2EXEPath !== undefined) - { + if (debugConfiguration.game !== undefined && debugConfiguration.request !== undefined) { + if (debugConfiguration.request === 'launch') { + if (debugConfiguration.launchType === 'MO2') { + if ( + debugConfiguration.mo2Config !== undefined && + debugConfiguration.mo2Config.shortcutURI !== undefined + ) { return debugConfiguration; } - } - else if (debugConfiguration.launchType === 'XSE') - { - if (debugConfiguration.XSELoaderPath !== undefined) - { + } else if (debugConfiguration.launchType === 'XSE') { + if (debugConfiguration.XSELoaderPath !== undefined) { return debugConfiguration; } } - } - else if (debugConfiguration.request === 'attach') - { + } else if (debugConfiguration.request === 'attach') { return debugConfiguration; } } - throw new Error("Invalid debug configuration."); + throw new Error('Invalid debug configuration.'); return undefined; } - + + // TODO: We might not want to do this + // substitute all the environment variables in the given string + // environment variables are of the form ${env:VARIABLE_NAME} async substituteEnvVars(string: string): Promise { - let appdata = process.env.LOCALAPPDATA; - let username = process.env.USERNAME; - if (appdata){ - string = string.replace('${env:LOCALAPPDATA}', appdata); - } - if (username){ - string = string.replace('${env:USERNAME}', username); + let envVars = string.match(/\$\{env:([^\}]+)\}/g); + if (envVars !== null) { + for (let envVar of envVars) { + if (envVar === undefined || envVar === null) { + continue; + } + let matches = envVar?.match(/\$\{env:([^\}]+)\}/); + if (matches === null || matches.length < 2) { + continue; + } + let envVarName = matches[1]; + let envVarValue: string | undefined; + + switch (envVarName) { + case 'LOCALAPPDATA': + envVarValue = getLocalAppDataFolder(); + break; + case 'USERNAME': + envVarValue = getUserName(); + break; + case 'HOMEPATH': + envVarValue = getHomeFolder(); + break; + case 'TEMP': + envVarValue = getTempFolder(); + break; + default: + envVarValue = undefined; + break; + } + + if (envVarValue === undefined) { + envVarValue = ''; + } + string = string.replace(envVar, envVarValue); + } } return string; } - // TODO: Check that all of these exist - async prepMo2Config(mo2Config: MO2Config, game: PapyrusGame): Promise { - let modFolder = mo2Config.modsFolder || await this._pathResolver.getModDirectoryPath(game); + async prepMo2Config(launcherPath: string, mo2Config: MO2Config, game: PapyrusGame): Promise { + let instanceINI = mo2Config.instanceIniPath; + if (!instanceINI) { + let { instanceName } = parseMoshortcutURI(mo2Config.shortcutURI); + let instanceInfo = await FindInstanceForEXE(launcherPath, instanceName); + if ( + instanceInfo && + GetPapyrusGameFromMO2GameID(instanceInfo.gameName) && + GetPapyrusGameFromMO2GameID(instanceInfo.gameName) === game + ) { + instanceINI = instanceInfo.iniPath; + } + } else { + instanceINI = mo2Config.instanceIniPath ? await this.substituteEnvVars(mo2Config.instanceIniPath) : mo2Config.instanceIniPath; + } return { - MO2EXEPath: await this.substituteEnvVars(mo2Config.MO2EXEPath), - shortcut: mo2Config.shortcut, - modsFolder: await this.substituteEnvVars(mo2Config.modsFolder || ""), - profile: mo2Config.profile || "Default", - profilesFolder: mo2Config.profilesFolder ? await this.substituteEnvVars(mo2Config?.profilesFolder) : undefined, - args: mo2Config.args || [] + shortcutURI: mo2Config.shortcutURI, + profile: mo2Config.profile, + instanceIniPath: instanceINI } as MO2Config; } @@ -128,37 +166,33 @@ export class PapyrusDebugConfigurationProvider implements DebugConfigurationProv debugConfiguration: IPapyrusDebugConfiguration, token?: CancellationToken ): Promise { - if (debugConfiguration.request === 'launch') - { - if (debugConfiguration.launchType === 'mo2') - { - if (debugConfiguration.mo2Config === undefined) - { - return undefined; - } - debugConfiguration.mo2Config = await this.prepMo2Config(debugConfiguration.mo2Config, debugConfiguration.game); - return debugConfiguration + if (debugConfiguration.request === 'launch' && debugConfiguration.launcherPath) { + let path = await this.substituteEnvVars(debugConfiguration.launcherPath); + if (path === undefined) { + throw new Error('Invalid debug configuration.'); } - - else if (debugConfiguration.launchType === 'XSE') - { - if(debugConfiguration.XSELoaderPath === undefined) - { - return undefined; + if (debugConfiguration.launchType === 'MO2') { + if (debugConfiguration.mo2Config === undefined) { + throw new Error('Invalid debug configuration.'); } - debugConfiguration.XSELoaderPath = await this.substituteEnvVars(debugConfiguration.XSELoaderPath); + debugConfiguration.mo2Config = await this.prepMo2Config( + path, + debugConfiguration.mo2Config, + debugConfiguration.game + ); + return debugConfiguration; + } else if (debugConfiguration.launchType === 'XSE') { return debugConfiguration; } } // else... - else if (debugConfiguration.request === 'attach') - { + else if (debugConfiguration.request === 'attach') { return debugConfiguration; } - throw new Error("Invalid debug configuration."); + throw new Error('Invalid debug configuration.'); return undefined; } - + dispose() { this._registration.dispose(); } diff --git a/src/papyrus-lang-vscode/src/debugger/PapyrusDebugSession.ts b/src/papyrus-lang-vscode/src/debugger/PapyrusDebugSession.ts index 3f994db5..3b651f70 100644 --- a/src/papyrus-lang-vscode/src/debugger/PapyrusDebugSession.ts +++ b/src/papyrus-lang-vscode/src/debugger/PapyrusDebugSession.ts @@ -7,52 +7,43 @@ export interface IPapyrusDebugSession extends DebugSession { } export interface MO2Config { - /** - * The path to the Mod Organizer 2 executable - * - * Example: - * - "C:/Program Files/Mod Organizer 2/ModOrganizer.exe" - */ - MO2EXEPath: string; /** * The shortcut URI for the Mod Organizer 2 profile to launch * - * You can get this from the Mod Organizer 2 shortcut menu + * You can get this from the Mod Organizer 2 shortcut menu. + * + * It is in the format: `moshortcut://:`. * - * Example: - * - "moshortcut://Skyrim Special Edition:SKSE" + * If the MO2 installation is portable, the instance name is blank. + * + * + * + * Examples: + * - non-portable: `moshortcut://Skyrim Special Edition:SKSE` + * - portable: `moshortcut://:F4SE` */ - shortcut: string; + shortcutURI: string; /** - * The path to the Mod Organizer 2 mods folder - * If not specified, defaults to the globally configured mods folder. + * The name of the Mod Organizer 2 profile to launch with. * - * Example: - * - "C:/Users/${USERNAME}/AppData/Local/ModOrganizer/Fallout 4/mods" - */ - modsFolder?: string; - /** - * The name of the Mod Organizer 2 profile to launch with - * Defaults to "Default" + * Defaults to the currently selected profile */ profile?: string; /** - * The path to the "profiles" folder for the Mod Organizer 2 instance. + * The path to the Mod Organizer 2 instance ini for this game. + * This is only necessary to be set if the debugger has difficulty finding the MO2 instance location * - * If you have specified a custom mods folder in your MO2 instance configuration, - * you must specify the profiles folder here. + * - If the Mod Organizer 2 exe is a portable installation, this is located in the parent folder. + * - If it is a non-portable installation, this in `%LOCALAPPDATA%/ModOrganizer//ModOrganizer.ini` * - * If not specified, defaults to the "profiles" folder in the same parent directory as the mods folder. - * - * Example: - * - "C:/Users/${USERNAME}/AppData/Local/ModOrganizer/Fallout 4/profiles" + * Examples: + * - `C:/Users//AppData/Local/ModOrganizer/Fallout4/ModOrganizer.ini` + * - `C:/Modding/MO2/ModOrganizer.ini` */ - profilesFolder?: string; + instanceIniPath?: string; +} - /** - * Additional arguments to pass to Mod Organizer 2 - */ - args?: string[]; +export interface XSEConfig { } export interface IPapyrusDebugConfiguration extends DebugConfiguration { @@ -71,34 +62,47 @@ export interface IPapyrusDebugConfiguration extends DebugConfiguration { * - 'launch': Launches the game */ request: 'attach' | 'launch'; - /** - * The type of launch to use - * - * - 'XSE': Launches the game using SKSE/F4SE without a mod manager - * - 'mo2': Launches the game using Mod Organizer 2 + + //TODO: split these into separate interfaces + /** + * The type of launcher to use + * + * - 'XSE': Launches the game using SKSE/F4SE without a mod manager + * - 'MO2': Launches the game using Mod Organizer 2 * */ - launchType?: 'XSE' | 'mo2'; + launchType?: 'XSE' | 'MO2'; + + /** + * The path to the launcher executable + * + * - If the launch type is 'MO2', this is the path to the Mod Organizer 2 executable. + * - If the launch type is 'XSE', this is the path to the f4se/skse loader executable. + * + * Examples: + * - "C:/Program Files/Mod Organizer 2/ModOrganizer.exe" + * - "C:/Program Files (x86)/Steam/steamapps/common/Skyrim Special Edition/skse64_loader.exe" + * - "C:/Program Files (x86)/Steam/steamapps/common/Fallout 4/f4se_loader.exe" + * + */ + launcherPath?: string; + /** * * Configuration for Mod Organizer 2 * - * Only used if launchType is 'mo2' + * Only used if launchType is 'MO2' * */ mo2Config?: MO2Config; - /** - * The path to the f4se/skse loader executable - * - * Examples: - * - "C:/Program Files (x86)/Steam/steamapps/common/Skyrim Special Edition/skse64_loader.exe" - * - "C:/Program Files (x86)/Steam/steamapps/common/Fallout 4/f4se_loader.exe" - */ - XSELoaderPath?: string; /** - * Additional arguments to pass - * */ + * (optional, advanced) Additional arguments to pass to the launcher + */ args?: string[]; + /** + * Ignore debugger configuration checks and launch + */ + ignoreConfigChecks?: boolean; } diff --git a/src/papyrus-lang-vscode/src/debugger/PexParser.ts b/src/papyrus-lang-vscode/src/debugger/PexParser.ts index a695ade7..5a4cd3ac 100644 --- a/src/papyrus-lang-vscode/src/debugger/PexParser.ts +++ b/src/papyrus-lang-vscode/src/debugger/PexParser.ts @@ -1,5 +1,5 @@ import { Parser } from 'binary-parser'; -import { readFileSync } from 'fs'; +import * as fs from 'fs'; import { PapyrusGame } from '../PapyrusGame'; // This interface contains the same members that are in the "Header" class in F:\workspace\skyrim-mod-workspace\Champollion\Pex\Header.hpp @@ -160,16 +160,16 @@ export class PexReader { .uint16('__strlen') .string('__string', { length: '__strlen', encoding: 'ascii', zeroTerminated: false }); - private readonly _strNest = { type: this.StringParser(), formatter: (x): string => x.__string }; + private readonly _strNest = { type: this.StringParser(), formatter: (x:any): string => x.__string }; private readonly StringTableParser = new Parser().uint16('__tbllen').array('__strings', { type: this.StringParser(), - formatter: (x): string[] => x.map((y) => y.__string), + formatter: (x:any): string[] => x.map((y:any) => y.__string), length: '__tbllen', }); - private readonly _strTableNest = { type: this.StringTableParser , formatter: (x): + private readonly _strTableNest = { type: this.StringTableParser , formatter: (x:any): PexStringTable => { // TODO: Global state hack to get around not being able to reference the parsed string table in the middle of the parse this.stringTable = new PexStringTable(x.__strings); @@ -186,14 +186,14 @@ export class PexReader { .array('LineNumbers', { type: this.GetUintType(), length: 'LineNumbersCount', - formatter: (x): number[] => x.map((y) => y.__val) + formatter: (x:any): number[] => x.map((y:any) => y.__val) }) private readonly FunctionInfosParser = () => new Parser().uint16('__FIlen').array('__infos', { type: this.FunctionInfoRawParser(), length: '__FIlen', - formatter: (x): FunctionInfo[] => x.map((y) => { + formatter: (x:any): FunctionInfo[] => x.map((y:any) => { let functinfo = { ObjectName: this.TableLookup(y.ObjectName), StateName: this.TableLookup(y.StateName), @@ -219,7 +219,7 @@ export class PexReader { .array('Names', { type: this.GetUintType(), length: 'NamesCount', - formatter: (x): number[] => x.map((y) => y.__val) + formatter: (x:any): number[] => x.map((y:any) => y.__val) }) private TableLookup (x: number){ if (x >= this.stringTable.strings.length){ @@ -230,13 +230,13 @@ export class PexReader { private readonly PropertyGroupsParser = () => new Parser().uint16('__PGlen').array('__infos', { type: this.PropertyGroupRawParser(), length: '__PGlen', - formatter: (x): PropertyGroup[] => x.map((y) => { + formatter: (x:any): PropertyGroup[] => x.map((y:any) => { let pgroups = { ObjectName: this.TableLookup(y.ObjectName), GroupName: this.TableLookup(y.GroupName), DocString: this.TableLookup(y.DocString), UserFlags: y.UserFlags, - Names: y.Names.map((z) => this.TableLookup(z)) + Names: y.Names.map((z:any) => this.TableLookup(z)) } as PropertyGroup; return pgroups; }) @@ -249,18 +249,18 @@ export class PexReader { .array('Names', { type: this.GetUintType(), length: 'NamesCount', - formatter: (x): number[] => x.map((y) => y.__val) + formatter: (x:any): number[] => x.map((y:any) => y.__val) }) private readonly StructOrdersParser = () => new Parser().uint16('__SOlen').array('__infos', { type: this.StructOrderRawParser(), length: '__SOlen', - formatter: (x): StructOrder[] => x.map((y) => { + formatter: (x:any): StructOrder[] => x.map((y:any) => { let sorders = { StructName: this.TableLookup(y.StructName), OrderName: this.TableLookup(y.OrderName), - Names: y.Names.map((z) => this.TableLookup(z)) + Names: y.Names.map((z:any) => this.TableLookup(z)) } as StructOrder; return sorders; }) @@ -271,11 +271,11 @@ export class PexReader { .uint64('ModificationTime') .nest('FunctionInfos', { type: this.FunctionInfosParser(), - formatter: (x): FunctionInfo[] => x.__infos + formatter: (x:any): FunctionInfo[] => x.__infos }) .nest('PropertyGroups', { type: this.PropertyGroupsParser(), - formatter: (x): PropertyGroup[] => x.__infos + formatter: (x:any): PropertyGroup[] => x.__infos }) .choice("StructOrders", { tag: () => { @@ -286,7 +286,7 @@ export class PexReader { 0: new Parser().skip(0), 1: this.StructOrdersParser() }, - formatter: (x): StructOrder[] | undefined => { + formatter: (x:any): StructOrder[] | undefined => { if (this.endianness === "little" && x){ return x.__infos; } @@ -304,7 +304,7 @@ export class PexReader { 0: new Parser().skip(0), 1: this._doParseDebugInfo() }, - formatter: (x): DebugInfo | undefined => { + formatter: (x:any): DebugInfo | undefined => { if (!x) { return undefined; } @@ -312,7 +312,7 @@ export class PexReader { } }) - private readonly _debugInfoNest = { type: this.ParseDebugInfo(), formatter: (x): DebugInfo | undefined => x ? x.DebugInfo : undefined }; + private readonly _debugInfoNest = { type: this.ParseDebugInfo(), formatter: (x:any): DebugInfo | undefined => x ? x.DebugInfo : undefined }; private readonly HeaderParser = () => new Parser() @@ -328,7 +328,7 @@ export class PexReader { private readonly _HeaderNest = (endianness: 'little' | 'big') => { return { type: this.HeaderParser(), - formatter: (x): PexHeader => { + formatter: (x: any): PexHeader => { return { Game: getGameFromEndianness(endianness), MajorVersion: x.MajorVersion, @@ -372,8 +372,11 @@ export class PexReader { public async ReadPexHeader(): Promise { // read the binary file from the path into a byte buffer - //let data = readFileSync(path,{encoding: 'binary'}); - const buffer = readFileSync(this.path); + if (!fs.existsSync(this.path) || !fs.lstatSync(this.path).isFile()) { + return undefined; + } + + const buffer = fs.readFileSync(this.path); if (!buffer || buffer.length < 4) { return undefined; } @@ -387,7 +390,7 @@ export class PexReader { } // not complete async ReadPex(): Promise{ - const buffer = readFileSync(this.path); + const buffer = fs.readFileSync(this.path); if (!buffer || buffer.length < 4) { return undefined; } diff --git a/src/papyrus-lang-vscode/src/features/commands/InstallDebugSupportCommand.ts b/src/papyrus-lang-vscode/src/features/commands/InstallDebugSupportCommand.ts index c38d7f3c..965991f3 100644 --- a/src/papyrus-lang-vscode/src/features/commands/InstallDebugSupportCommand.ts +++ b/src/papyrus-lang-vscode/src/features/commands/InstallDebugSupportCommand.ts @@ -1,10 +1,12 @@ import { IDebugSupportInstallService, DebugSupportInstallState } from '../../debugger/DebugSupportInstallService'; import { window, ProgressLocation } from 'vscode'; -import { PapyrusGame, getDisplayNameForGame } from "../../PapyrusGame"; +import { PapyrusGame, getDisplayNameForGame } from '../../PapyrusGame'; import { GameCommandBase } from './GameCommandBase'; import { getGameIsRunning } from '../../Utilities'; -import { waitWhile } from "../../VsCodeUtilities"; +import { waitWhile } from '../../VsCodeUtilities'; import { inject, injectable } from 'inversify'; +import { IMO2ConfiguratorService, MO2ConfiguratorService } from '../../debugger/MO2ConfiguratorService'; +import { IMO2LauncherDescriptor } from '../../debugger/MO2LaunchDescriptorFactory'; export function showGameDisabledMessage(game: PapyrusGame) { window.showErrorMessage( @@ -23,14 +25,39 @@ export function showGameMissingMessage(game: PapyrusGame) { @injectable() export class InstallDebugSupportCommand extends GameCommandBase { private readonly _installer: IDebugSupportInstallService; - - constructor(@inject(IDebugSupportInstallService) installer: IDebugSupportInstallService) { + private readonly _mo2ConfiguratorService: IMO2ConfiguratorService; + constructor( + @inject(IDebugSupportInstallService) installer: IDebugSupportInstallService, + @inject(IMO2ConfiguratorService) mo2ConfiguratorService: IMO2ConfiguratorService + ) { super('installDebuggerSupport', [PapyrusGame.fallout4, PapyrusGame.skyrimSpecialEdition]); this._installer = installer; + this._mo2ConfiguratorService = mo2ConfiguratorService; + } + + // TODO: Fix the args + protected getLauncherDescriptor(...args: [any | undefined]): IMO2LauncherDescriptor | undefined { + // If we have args, it's a debugger launch. + if (args.length > 0) { + // args 0 indicates the launch type + let launchArgs: any[] = args[0]; + if (launchArgs.length < 1) { + return; + } + let launchType = launchArgs[0] as string; + if (launchType === 'XSE') { + // do stuff + } + if (launchArgs.length > 1 && launchType === 'MO2') { + return launchArgs[1] as IMO2LauncherDescriptor; + } + } + return undefined; } - protected async onExecute(game: PapyrusGame) { + protected async onExecute(game: PapyrusGame, ...args: [any | undefined]) { + let launcherDescriptor = this.getLauncherDescriptor(...args); const installed = await window.withProgress( { cancellable: true, @@ -59,7 +86,9 @@ export class InstallDebugSupportCommand extends GameCommandBase { return false; } - return await this._installer.installPlugin(game, token); + return launcherDescriptor ? + await this._mo2ConfiguratorService.fixDebuggerConfiguration(launcherDescriptor, token) : + await this._installer.installPlugin(game, token); } catch (error) { window.showErrorMessage( `Failed to install Papyrus debugger support for ${getDisplayNameForGame(game)}: ${error}` @@ -70,7 +99,9 @@ export class InstallDebugSupportCommand extends GameCommandBase { } ); - const currentStatus = await this._installer.getInstallState(game); + const currentStatus = launcherDescriptor + ? await this._mo2ConfiguratorService.getStateFromConfig(launcherDescriptor) + : await this._installer.getInstallState(game); if (installed) { if (currentStatus === DebugSupportInstallState.installedAsMod) { diff --git a/src/papyrus-lang-vscode/tsconfig.json b/src/papyrus-lang-vscode/tsconfig.json index d21b6d53..eb916f8c 100644 --- a/src/papyrus-lang-vscode/tsconfig.json +++ b/src/papyrus-lang-vscode/tsconfig.json @@ -5,11 +5,15 @@ "allowSyntheticDefaultImports": true, "target": "es6", "outDir": "out", - "lib": ["es6"], + "lib": [ + "es6" + ], "sourceMap": true, "rootDir": "src", "experimentalDecorators": true, "strict": true }, - "include": ["src/**/*.ts"] -} + "include": [ + "src/**/*.ts" + ] +} \ No newline at end of file From 73a7c9e0987eccc6cb1917e03e12d8d7f8f3cf48 Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Sun, 1 Oct 2023 16:17:29 -0700 Subject: [PATCH 08/15] debugserver: reminder comment --- src/DarkId.Papyrus.DebugServer/PexCache.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DarkId.Papyrus.DebugServer/PexCache.cpp b/src/DarkId.Papyrus.DebugServer/PexCache.cpp index a00c0f5c..d6309a6b 100644 --- a/src/DarkId.Papyrus.DebugServer/PexCache.cpp +++ b/src/DarkId.Papyrus.DebugServer/PexCache.cpp @@ -80,7 +80,7 @@ namespace DarkId::Papyrus::DebugServer } data.name = normname; data.path = headerSrcName; - data.sourceReference = sourceReference; + data.sourceReference = sourceReference; // TODO: Remember to remove this when we get script references from the extension working return true; } From 4485672238d1773d0b85883966026fb7954c1db7 Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Sun, 1 Oct 2023 16:17:29 -0700 Subject: [PATCH 09/15] debugserver: fix f4 execution event install --- src/DarkId.Papyrus.DebugServer/RuntimeEvents.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DarkId.Papyrus.DebugServer/RuntimeEvents.cpp b/src/DarkId.Papyrus.DebugServer/RuntimeEvents.cpp index 93d79702..aedb606a 100644 --- a/src/DarkId.Papyrus.DebugServer/RuntimeEvents.cpp +++ b/src/DarkId.Papyrus.DebugServer/RuntimeEvents.cpp @@ -523,7 +523,7 @@ namespace DarkId::Papyrus::DebugServer if (tasklet->topFrame) { // We don't need to set the instruction pointer because Fallout 4 assigns the IP every time an opcode is executed - g_InstructionExecutionEvent(tasklet, tasklet->topFrame->STACK_FRAME_IP); + g_InstructionExecutionEvent(tasklet); } } // TODO: There's a second CreateStack() @ 1427422C0, do we need to hook that? From f7835028ad387a00ee9e21dda3e6e64b50989a12 Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Sun, 1 Oct 2023 16:17:29 -0700 Subject: [PATCH 10/15] Fix getGameINIFromMO2Profile --- src/papyrus-lang-vscode/src/debugger/MO2Helpers.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/papyrus-lang-vscode/src/debugger/MO2Helpers.ts b/src/papyrus-lang-vscode/src/debugger/MO2Helpers.ts index fc5493b7..4aabc931 100644 --- a/src/papyrus-lang-vscode/src/debugger/MO2Helpers.ts +++ b/src/papyrus-lang-vscode/src/debugger/MO2Helpers.ts @@ -152,7 +152,9 @@ export async function getGameINIFromMO2Profile( // Game ini paths for MO2 are different depending on whether the profile has local settings or not // if [General] LocalSettings=false, then the game ini is in the global game save folder // if [General] LocalSettings=true, then the game ini is in the profile folder - const settingsIniData = await getMO2ProfileSettingsData(profileFolder); + + const settingsFile = path.join(profileFolder, 'settings.ini') + const settingsIniData = await getMO2ProfileSettingsData(settingsFile); if (!settingsIniData) { throw new Error(`Could not get settings ini data`); } From e86358242e267cff7d70a8f619d4e6f6c3ab311e Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Sun, 1 Oct 2023 16:17:29 -0700 Subject: [PATCH 11/15] fix address library install --- src/papyrus-lang-vscode/src/debugger/AddLibHelpers.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/papyrus-lang-vscode/src/debugger/AddLibHelpers.ts b/src/papyrus-lang-vscode/src/debugger/AddLibHelpers.ts index 5fd6475e..373685c9 100644 --- a/src/papyrus-lang-vscode/src/debugger/AddLibHelpers.ts +++ b/src/papyrus-lang-vscode/src/debugger/AddLibHelpers.ts @@ -343,6 +343,9 @@ export async function _checkAddressLibsInstalled( return AddressLibInstalledState.installed; } +// TODO: For some godforsaken reason, the address library names on Nexus mods for both SE and AE are the same. +// (i.e. "Address Library for SKSE Plugins") +// Need to handle this export async function _installAddressLibs( game: PapyrusGame, ParentInstallDir: string, @@ -370,7 +373,7 @@ export async function _installAddressLibs( throw new Error('Asset list is corrupt'); } const zipPath = path.join(downloadDir, asset.zipFile); - fs.rmSync(addressLibInstallPath, { recursive: true, force: true }); + // fs.rmSync(addressLibInstallPath, { recursive: true, force: true }); await mkdirIfNeeded(addressLibInstallPath); await extractZip(zipPath, { dir: addressLibInstallPath, From 01923cdc047648faa1ea07237e4df9788773205e Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Sun, 1 Oct 2023 16:22:36 -0700 Subject: [PATCH 12/15] add missing packages --- src/papyrus-lang-vscode/package-lock.json | 720 +++++++++++++++++++++- src/papyrus-lang-vscode/package.json | 3 + 2 files changed, 689 insertions(+), 34 deletions(-) diff --git a/src/papyrus-lang-vscode/package-lock.json b/src/papyrus-lang-vscode/package-lock.json index 5b9b3459..948d19bf 100644 --- a/src/papyrus-lang-vscode/package-lock.json +++ b/src/papyrus-lang-vscode/package-lock.json @@ -9,9 +9,12 @@ "version": "3.0.0", "dependencies": { "@semantic-release/exec": "^6.0.3", + "@terascope/fetch-github-release": "^0.8.7", "@tybys/windows-file-version-info": "^1.0.5", "@types/semantic-release": "^17.2.4", + "binary-parser": "^2.2.1", "deepmerge": "^4.2.2", + "extract-zip": "^2.0.1", "fast-deep-equal": "^3.1.3", "ini": "^3.0.1", "inversify": "^6.0.1", @@ -744,6 +747,82 @@ "semantic-release": ">=18.0.0-beta.1" } }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@terascope/fetch-github-release": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@terascope/fetch-github-release/-/fetch-github-release-0.8.7.tgz", + "integrity": "sha512-m6vpKCUlBYhxlx6BXQoOi0hg/FFZ5Bo/De+MjhDuoFLhR8pMzJxS0Nge8bSIXc1G8Jqmw4g4mkoZOqEb9TmMbQ==", + "dependencies": { + "extract-zip": "^2.0.1", + "gauge": "^3.0.0", + "got": "^11.4.0", + "multi-progress": "^4.0.0", + "progress": "^2.0.3", + "yargs": "^17.2.1" + }, + "bin": { + "fetch-github-release": "bin/fetch-github-release" + } + }, + "node_modules/@terascope/fetch-github-release/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@terascope/fetch-github-release/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@terascope/fetch-github-release/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -783,6 +862,17 @@ "integrity": "sha512-a5s4m8fFCf/bp+KcawwPTxk1ptftmWWAvsIxorI/K92DgXcCtqIvhW3z7WzXMl4E0yep+WoHTfIz4tnJldjnhg==", "hasInstallScript": true }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "node_modules/@types/eslint": { "version": "8.4.10", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.10.tgz", @@ -809,6 +899,11 @@ "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", "dev": true }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.2.tgz", + "integrity": "sha512-FD+nQWA2zJjh4L9+pFXqWOi0Hs1ryBCfI+985NjluQ1p8EYtoLvjLOKidXBtZ4/IcxDX4o8/E8qDS3540tNliw==" + }, "node_modules/@types/ini": { "version": "1.3.31", "resolved": "https://registry.npmjs.org/@types/ini/-/ini-1.3.31.tgz", @@ -821,6 +916,14 @@ "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", "dev": true }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/minimist": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", @@ -847,6 +950,14 @@ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" }, + "node_modules/@types/responselike": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.1.tgz", + "integrity": "sha512-TiGnitEDxj2X0j+98Eqk5lv/Cij8oHd32bU4D/Yw6AOq7vvTk0gSD2GPj0G/HkvhMoVsdlhYF4yqqlyPBTM6Sg==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", @@ -878,6 +989,15 @@ "integrity": "sha512-SDatEMEtQ1cJK3esIdH6colduWBP+42Xw9Guq1sf/N6rM3ZxgljBduvZOwBsxRps/k5+Wwf5HJun6pH8OnD2gg==", "dev": true }, + "node_modules/@types/yauzl": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.1.tgz", + "integrity": "sha512-CHzgNU3qYBnp/O4S3yv2tXPlvMTq0YWSTVg2/JYLqWZGHwwgJGAwd00poay/11asPq8wLFwHzubyInqHIFmmiw==", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.7.3", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.3.tgz", @@ -1476,6 +1596,11 @@ "node": ">= 8" } }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -1572,6 +1697,14 @@ "node": ">=8" } }, + "node_modules/binary-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/binary-parser/-/binary-parser-2.2.1.tgz", + "integrity": "sha512-5ATpz/uPDgq5GgEDxTB4ouXCde7q2lqAQlSdBRQVl/AJnxmQmhIfyxJx+0MGu//D5rHQifkfGbWWlaysG0o9NA==", + "engines": { + "node": ">=12" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -1699,7 +1832,6 @@ "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, "engines": { "node": "*" } @@ -1743,6 +1875,45 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -1980,6 +2151,25 @@ "node": ">=6" } }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clone-response/node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "engines": { + "node": ">=4" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -1993,6 +2183,14 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/colorette": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", @@ -2019,6 +2217,11 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, "node_modules/conventional-changelog-angular": { "version": "5.0.13", "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz", @@ -2270,8 +2473,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "optional": true, "dependencies": { "mimic-response": "^3.1.0" }, @@ -2448,6 +2649,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "engines": { + "node": ">=10" + } + }, "node_modules/define-lazy-prop": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", @@ -2631,8 +2840,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "dev": true, - "optional": true, "dependencies": { "once": "^1.4.0" } @@ -3156,6 +3363,39 @@ "node": ">=6" } }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3215,7 +3455,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, "dependencies": { "pend": "~1.2.0" } @@ -3511,6 +3750,25 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -3663,6 +3921,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, "node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -3733,6 +4015,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, "node_modules/hook-std": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hook-std/-/hook-std-2.0.0.tgz", @@ -3771,6 +4058,11 @@ "entities": "^4.3.0" } }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" + }, "node_modules/http-proxy-agent": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", @@ -3785,6 +4077,29 @@ "node": ">= 6" } }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/http2-wrapper/node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -4244,8 +4559,7 @@ "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" }, "node_modules/json-parse-better-errors": { "version": "1.0.2", @@ -4324,7 +4638,6 @@ "version": "4.5.3", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", - "dev": true, "dependencies": { "json-buffer": "3.0.1" } @@ -4474,6 +4787,14 @@ "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", "integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==" }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "engines": { + "node": ">=8" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -4702,8 +5023,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "optional": true, "engines": { "node": ">=10" }, @@ -4783,6 +5102,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/multi-progress": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/multi-progress/-/multi-progress-4.0.0.tgz", + "integrity": "sha512-9zcjyOou3FFCKPXsmkbC3ethv51SFPoA4dJD6TscIp2pUmy26kBDZW6h9XofPELrzseSkuD7r0V+emGEeo39Pg==", + "peerDependencies": { + "progress": "^2.0.0" + } + }, "node_modules/mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", @@ -7283,6 +7610,14 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", @@ -7349,6 +7684,14 @@ "node": ">= 0.8.0" } }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "engines": { + "node": ">=8" + } + }, "node_modules/p-each-series": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz", @@ -7565,8 +7908,7 @@ "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" }, "node_modules/picocolors": { "version": "1.0.0", @@ -7711,6 +8053,14 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ps-list": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/ps-list/-/ps-list-6.3.0.tgz", @@ -7723,8 +8073,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "optional": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -8031,6 +8379,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + }, "node_modules/resolve-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", @@ -8051,6 +8404,17 @@ "node": ">=8" } }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -9460,6 +9824,14 @@ "node": ">= 8" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/wildcard": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", @@ -9618,7 +9990,6 @@ "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" @@ -10174,6 +10545,63 @@ "read-pkg-up": "^7.0.0" } }, + "@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==" + }, + "@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "requires": { + "defer-to-connect": "^2.0.0" + } + }, + "@terascope/fetch-github-release": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@terascope/fetch-github-release/-/fetch-github-release-0.8.7.tgz", + "integrity": "sha512-m6vpKCUlBYhxlx6BXQoOi0hg/FFZ5Bo/De+MjhDuoFLhR8pMzJxS0Nge8bSIXc1G8Jqmw4g4mkoZOqEb9TmMbQ==", + "requires": { + "extract-zip": "^2.0.1", + "gauge": "^3.0.0", + "got": "^11.4.0", + "multi-progress": "^4.0.0", + "progress": "^2.0.3", + "yargs": "^17.2.1" + }, + "dependencies": { + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" + } + } + }, "@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -10209,6 +10637,17 @@ "resolved": "https://registry.npmjs.org/@tybys/windows-file-version-info/-/windows-file-version-info-1.0.5.tgz", "integrity": "sha512-a5s4m8fFCf/bp+KcawwPTxk1ptftmWWAvsIxorI/K92DgXcCtqIvhW3z7WzXMl4E0yep+WoHTfIz4tnJldjnhg==" }, + "@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "requires": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "@types/eslint": { "version": "8.4.10", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.10.tgz", @@ -10235,6 +10674,11 @@ "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", "dev": true }, + "@types/http-cache-semantics": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.2.tgz", + "integrity": "sha512-FD+nQWA2zJjh4L9+pFXqWOi0Hs1ryBCfI+985NjluQ1p8EYtoLvjLOKidXBtZ4/IcxDX4o8/E8qDS3540tNliw==" + }, "@types/ini": { "version": "1.3.31", "resolved": "https://registry.npmjs.org/@types/ini/-/ini-1.3.31.tgz", @@ -10247,6 +10691,14 @@ "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", "dev": true }, + "@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "requires": { + "@types/node": "*" + } + }, "@types/minimist": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", @@ -10273,6 +10725,14 @@ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" }, + "@types/responselike": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.1.tgz", + "integrity": "sha512-TiGnitEDxj2X0j+98Eqk5lv/Cij8oHd32bU4D/Yw6AOq7vvTk0gSD2GPj0G/HkvhMoVsdlhYF4yqqlyPBTM6Sg==", + "requires": { + "@types/node": "*" + } + }, "@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", @@ -10304,6 +10764,15 @@ "integrity": "sha512-SDatEMEtQ1cJK3esIdH6colduWBP+42Xw9Guq1sf/N6rM3ZxgljBduvZOwBsxRps/k5+Wwf5HJun6pH8OnD2gg==", "dev": true }, + "@types/yauzl": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.1.tgz", + "integrity": "sha512-CHzgNU3qYBnp/O4S3yv2tXPlvMTq0YWSTVg2/JYLqWZGHwwgJGAwd00poay/11asPq8wLFwHzubyInqHIFmmiw==", + "optional": true, + "requires": { + "@types/node": "*" + } + }, "@typescript-eslint/eslint-plugin": { "version": "6.7.3", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.3.tgz", @@ -10735,6 +11204,11 @@ "picomatch": "^2.0.4" } }, + "aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, "arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -10816,6 +11290,11 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "binary-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/binary-parser/-/binary-parser-2.2.1.tgz", + "integrity": "sha512-5ATpz/uPDgq5GgEDxTB4ouXCde7q2lqAQlSdBRQVl/AJnxmQmhIfyxJx+0MGu//D5rHQifkfGbWWlaysG0o9NA==" + }, "bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -10905,8 +11384,7 @@ "buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==" }, "buffer-from": { "version": "1.1.2", @@ -10935,6 +11413,35 @@ "run-applescript": "^5.0.0" } }, + "cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==" + }, + "cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + } + } + }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -11103,6 +11610,21 @@ "shallow-clone": "^3.0.0" } }, + "clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "requires": { + "mimic-response": "^1.0.0" + }, + "dependencies": { + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + } + } + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -11116,6 +11638,11 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, + "color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" + }, "colorette": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", @@ -11142,6 +11669,11 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, "conventional-changelog-angular": { "version": "5.0.13", "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz", @@ -11327,8 +11859,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "optional": true, "requires": { "mimic-response": "^3.1.0" } @@ -11438,6 +11968,11 @@ "untildify": "^4.0.0" } }, + "defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==" + }, "define-lazy-prop": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", @@ -11572,8 +12107,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "dev": true, - "optional": true, "requires": { "once": "^1.4.0" } @@ -11928,6 +12461,27 @@ "dev": true, "optional": true }, + "extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + } + } + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -11981,7 +12535,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, "requires": { "pend": "~1.2.0" } @@ -12195,6 +12748,22 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "requires": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + } + }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -12312,6 +12881,24 @@ "slash": "^3.0.0" } }, + "got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "requires": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + } + }, "graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -12359,6 +12946,11 @@ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "dev": true }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, "hook-std": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hook-std/-/hook-std-2.0.0.tgz", @@ -12384,6 +12976,11 @@ "entities": "^4.3.0" } }, + "http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" + }, "http-proxy-agent": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", @@ -12395,6 +12992,22 @@ "debug": "4" } }, + "http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "requires": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "dependencies": { + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" + } + } + }, "https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -12699,8 +13312,7 @@ "json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" }, "json-parse-better-errors": { "version": "1.0.2", @@ -12767,7 +13379,6 @@ "version": "4.5.3", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", - "dev": true, "requires": { "json-buffer": "3.0.1" } @@ -12897,6 +13508,11 @@ "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", "integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==" }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -13055,9 +13671,7 @@ "mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "optional": true + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" }, "min-indent": { "version": "1.0.1", @@ -13113,6 +13727,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "multi-progress": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/multi-progress/-/multi-progress-4.0.0.tgz", + "integrity": "sha512-9zcjyOou3FFCKPXsmkbC3ethv51SFPoA4dJD6TscIp2pUmy26kBDZW6h9XofPELrzseSkuD7r0V+emGEeo39Pg==", + "requires": {} + }, "mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", @@ -14804,6 +15424,11 @@ "boolbase": "^1.0.0" } }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, "object-inspect": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", @@ -14852,6 +15477,11 @@ "type-check": "^0.4.0" } }, + "p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==" + }, "p-each-series": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz", @@ -15006,8 +15636,7 @@ "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" }, "picocolors": { "version": "1.0.0", @@ -15112,6 +15741,11 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" + }, "ps-list": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/ps-list/-/ps-list-6.3.0.tgz", @@ -15121,8 +15755,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "optional": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -15351,6 +15983,11 @@ "supports-preserve-symlinks-flag": "^1.0.0" } }, + "resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + }, "resolve-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", @@ -15365,6 +16002,14 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" }, + "responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "requires": { + "lowercase-keys": "^2.0.0" + } + }, "retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -16373,6 +17018,14 @@ "isexe": "^2.0.0" } }, + "wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "requires": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "wildcard": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", @@ -16494,7 +17147,6 @@ "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, "requires": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" diff --git a/src/papyrus-lang-vscode/package.json b/src/papyrus-lang-vscode/package.json index 8a2821ec..89efed10 100644 --- a/src/papyrus-lang-vscode/package.json +++ b/src/papyrus-lang-vscode/package.json @@ -549,9 +549,12 @@ ], "dependencies": { "@semantic-release/exec": "^6.0.3", + "@terascope/fetch-github-release": "^0.8.7", "@tybys/windows-file-version-info": "^1.0.5", "@types/semantic-release": "^17.2.4", + "binary-parser": "^2.2.1", "deepmerge": "^4.2.2", + "extract-zip": "^2.0.1", "fast-deep-equal": "^3.1.3", "ini": "^3.0.1", "inversify": "^6.0.1", From 22fb2370d445dd60c2cd036ee6c9b5fe64dfd1d4 Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Sun, 1 Oct 2023 16:51:23 -0700 Subject: [PATCH 13/15] add "unused-imports" plugin to auto-fix unused imports --- src/papyrus-lang-vscode/.eslintrc.js | 9 ++++- src/papyrus-lang-vscode/package-lock.json | 46 +++++++++++++++++++++++ src/papyrus-lang-vscode/package.json | 1 + 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/papyrus-lang-vscode/.eslintrc.js b/src/papyrus-lang-vscode/.eslintrc.js index d95563ba..4922fd15 100644 --- a/src/papyrus-lang-vscode/.eslintrc.js +++ b/src/papyrus-lang-vscode/.eslintrc.js @@ -9,10 +9,15 @@ module.exports = { overrides: [ { files: ['*.ts'], - plugins: ['@typescript-eslint'], + plugins: ['@typescript-eslint', "unused-imports"], extends: ['plugin:@typescript-eslint/recommended'], rules: { - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + '@typescript-eslint/no-unused-vars': "off", + "unused-imports/no-unused-imports": "error", + "unused-imports/no-unused-vars": [ + "error", + { "vars": "all", "varsIgnorePattern": "^_", "args": "after-used", "argsIgnorePattern": "^_" } + ] }, }, ], diff --git a/src/papyrus-lang-vscode/package-lock.json b/src/papyrus-lang-vscode/package-lock.json index 948d19bf..09b6dfae 100644 --- a/src/papyrus-lang-vscode/package-lock.json +++ b/src/papyrus-lang-vscode/package-lock.json @@ -45,6 +45,7 @@ "eslint": "^8.50.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", + "eslint-plugin-unused-imports": "^3.0.0", "fork-ts-checker-webpack-plugin": "^7.2.14", "prettier": "^3.0.3", "rimraf": "^3.0.2", @@ -3019,6 +3020,36 @@ } } }, + "node_modules/eslint-plugin-unused-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-3.0.0.tgz", + "integrity": "sha512-sduiswLJfZHeeBJ+MQaG+xYzSWdRXoSw61DpU13mzWumCkR0ufD0HmO4kdNokjrkluMHpj/7PJeN35pgbhW3kw==", + "dev": true, + "dependencies": { + "eslint-rule-composer": "^0.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^6.0.0", + "eslint": "^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } + } + }, + "node_modules/eslint-rule-composer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", + "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -12354,6 +12385,21 @@ "synckit": "^0.8.5" } }, + "eslint-plugin-unused-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-3.0.0.tgz", + "integrity": "sha512-sduiswLJfZHeeBJ+MQaG+xYzSWdRXoSw61DpU13mzWumCkR0ufD0HmO4kdNokjrkluMHpj/7PJeN35pgbhW3kw==", + "dev": true, + "requires": { + "eslint-rule-composer": "^0.3.0" + } + }, + "eslint-rule-composer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", + "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", + "dev": true + }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", diff --git a/src/papyrus-lang-vscode/package.json b/src/papyrus-lang-vscode/package.json index 89efed10..9376bed7 100644 --- a/src/papyrus-lang-vscode/package.json +++ b/src/papyrus-lang-vscode/package.json @@ -586,6 +586,7 @@ "eslint": "^8.50.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", + "eslint-plugin-unused-imports": "^3.0.0", "fork-ts-checker-webpack-plugin": "^7.2.14", "prettier": "^3.0.3", "rimraf": "^3.0.2", From 6333d1a28d9900c921f8e65cbf52f7d8a92e66db Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Sun, 1 Oct 2023 16:51:47 -0700 Subject: [PATCH 14/15] rerun eslint --- .../src/CreationKitInfoProvider.ts | 2 +- .../src/PapyrusExtension.ts | 8 +- src/papyrus-lang-vscode/src/PapyrusGame.ts | 24 +- src/papyrus-lang-vscode/src/Utilities.ts | 85 ++- .../src/VsCodeUtilities.ts | 1 - src/papyrus-lang-vscode/src/WorkspaceGame.ts | 6 +- .../src/common/GameHelpers.ts | 308 ++++----- .../src/common/GithubHelpers.ts | 74 ++- .../src/common/INIHelpers.ts | 91 ++- src/papyrus-lang-vscode/src/common/MO2Lib.ts | 211 +++--- .../src/common/OSHelpers.ts | 32 +- .../src/common/PathResolver.ts | 10 +- .../src/common/constants.ts | 15 +- .../src/debugger/AddLibHelpers.ts | 18 +- .../src/debugger/AddressLibInstallService.ts | 19 +- .../src/debugger/DebugLauncherService.ts | 11 +- .../debugger/DebugSupportInstallService.ts | 4 +- .../debugger/GameDebugConfiguratorService.ts | 15 +- .../src/debugger/MO2ConfiguratorService.ts | 49 +- .../src/debugger/MO2Helpers.ts | 46 +- .../debugger/MO2LaunchDescriptorFactory.ts | 12 +- .../PapyrusDebugAdapterDescriptorFactory.ts | 108 ++-- .../debugger/PapyrusDebugAdapterTracker.ts | 8 +- .../PapyrusDebugConfigurationProvider.ts | 40 +- .../src/debugger/PapyrusDebugSession.ts | 40 +- .../src/debugger/PexParser.ts | 600 +++++++++--------- .../features/LanguageServiceStatusItems.ts | 2 +- .../src/features/PyroTaskProvider.ts | 4 +- .../commands/AttachDebuggerCommand.ts | 2 +- .../src/features/commands/GameCommandBase.ts | 2 +- .../commands/GenerateProjectCommand.ts | 2 +- .../commands/InstallDebugSupportCommand.ts | 16 +- .../commands/SearchCreationKitWikiCommand.ts | 2 +- .../projects/ProjectsTreeDataProvider.ts | 2 +- .../src/server/LanguageClient.ts | 2 +- .../src/server/LanguageClientHost.ts | 4 +- .../src/server/LanguageClientManager.ts | 2 +- 37 files changed, 949 insertions(+), 928 deletions(-) diff --git a/src/papyrus-lang-vscode/src/CreationKitInfoProvider.ts b/src/papyrus-lang-vscode/src/CreationKitInfoProvider.ts index 344f6cc6..98b8fb26 100644 --- a/src/papyrus-lang-vscode/src/CreationKitInfoProvider.ts +++ b/src/papyrus-lang-vscode/src/CreationKitInfoProvider.ts @@ -1,5 +1,5 @@ import { interfaces, inject, injectable } from 'inversify'; -import { PapyrusGame, getGames, getDevelopmentCompilerFolderForGame } from "./PapyrusGame"; +import { PapyrusGame, getGames, getDevelopmentCompilerFolderForGame } from './PapyrusGame'; import { IExtensionConfigProvider } from './ExtensionConfigProvider'; import { Observable, combineLatest } from 'rxjs'; import { map, mergeMap, shareReplay } from 'rxjs/operators'; diff --git a/src/papyrus-lang-vscode/src/PapyrusExtension.ts b/src/papyrus-lang-vscode/src/PapyrusExtension.ts index 5b6cf4a5..cbd7a44f 100644 --- a/src/papyrus-lang-vscode/src/PapyrusExtension.ts +++ b/src/papyrus-lang-vscode/src/PapyrusExtension.ts @@ -26,10 +26,10 @@ import { GenerateProjectCommand } from './features/commands/GenerateProjectComma import { showWelcome } from './features/WelcomeHandler'; import { ShowWelcomeCommand } from './features/commands/ShowWelcomeCommand'; import { Container } from 'inversify'; -import { IDebugLauncherService, DebugLauncherService } from "./debugger/DebugLauncherService"; -import { IAddressLibraryInstallService, AddressLibraryInstallService } from "./debugger/AddressLibInstallService"; -import { IMO2LaunchDescriptorFactory, MO2LaunchDescriptorFactory } from "./debugger/MO2LaunchDescriptorFactory"; -import { IMO2ConfiguratorService, MO2ConfiguratorService } from "./debugger/MO2ConfiguratorService"; +import { IDebugLauncherService, DebugLauncherService } from './debugger/DebugLauncherService'; +import { IAddressLibraryInstallService, AddressLibraryInstallService } from './debugger/AddressLibInstallService'; +import { IMO2LaunchDescriptorFactory, MO2LaunchDescriptorFactory } from './debugger/MO2LaunchDescriptorFactory'; +import { IMO2ConfiguratorService, MO2ConfiguratorService } from './debugger/MO2ConfiguratorService'; class PapyrusExtension implements Disposable { private readonly _serviceContainer: Container; diff --git a/src/papyrus-lang-vscode/src/PapyrusGame.ts b/src/papyrus-lang-vscode/src/PapyrusGame.ts index 198fc527..7395bb7f 100644 --- a/src/papyrus-lang-vscode/src/PapyrusGame.ts +++ b/src/papyrus-lang-vscode/src/PapyrusGame.ts @@ -89,35 +89,35 @@ export function getExecutableNameForGame(game: PapyrusGame) { } export function getGameIniName(game: PapyrusGame): string { - return game == PapyrusGame.fallout4 ? 'fallout4.ini' :'skyrim.ini'; + return game == PapyrusGame.fallout4 ? 'fallout4.ini' : 'skyrim.ini'; } // TODO: Support VR export enum GameVariant { - Steam = "Steam", - GOG = "GOG", - Epic = "Epic Games" + Steam = 'Steam', + GOG = 'GOG', + Epic = 'Epic Games', } /** * returns the name of the Game Save folder for the given variant - * @param variant - * @returns + * @param variant + * @returns */ -export function GetUserGameFolderName(game: PapyrusGame, variant: GameVariant){ +export function GetUserGameFolderName(game: PapyrusGame, variant: GameVariant) { switch (game) { case PapyrusGame.fallout4: - return "Fallout4"; + return 'Fallout4'; case PapyrusGame.skyrim: - return "Skyrim"; + return 'Skyrim'; case PapyrusGame.skyrimSpecialEdition: switch (variant) { case GameVariant.Steam: - return "Skyrim Special Edition"; + return 'Skyrim Special Edition'; case GameVariant.GOG: - return "Skyrim Special Edition GOG"; + return 'Skyrim Special Edition GOG'; case GameVariant.Epic: - return "Skyrim Special Edition EPIC"; + return 'Skyrim Special Edition EPIC'; } } } diff --git a/src/papyrus-lang-vscode/src/Utilities.ts b/src/papyrus-lang-vscode/src/Utilities.ts index ec68e3bf..ff44c540 100644 --- a/src/papyrus-lang-vscode/src/Utilities.ts +++ b/src/papyrus-lang-vscode/src/Utilities.ts @@ -5,13 +5,11 @@ import { promisify } from 'util'; import procList from 'ps-list'; -import { getExecutableNameForGame, PapyrusGame } from "./PapyrusGame"; +import { getExecutableNameForGame, PapyrusGame } from './PapyrusGame'; import { isNativeError } from 'util/types'; -import { - getSystemErrorMap -} from "util"; +import { getSystemErrorMap } from 'util'; import { execFile as _execFile } from 'child_process'; const execFile = promisify(_execFile); const readFile = promisify(fs.readFile); @@ -39,8 +37,10 @@ export async function getGameIsRunning(game: PapyrusGame) { export async function getGamePIDs(game: PapyrusGame): Promise> { const processList = await procList(); - - const gameProcesses = processList.filter((p) => p.name.toLowerCase() === getExecutableNameForGame(game).toLowerCase()); + + const gameProcesses = processList.filter( + (p) => p.name.toLowerCase() === getExecutableNameForGame(game).toLowerCase() + ); if (gameProcesses.length === 0) { return []; @@ -51,7 +51,7 @@ export async function getGamePIDs(game: PapyrusGame): Promise> { export async function getPIDforProcessName(processName: string): Promise> { const processList = await procList(); - let thing = processList[0]; + const thing = processList[0]; const gameProcesses = processList.filter((p) => p.name.toLowerCase() === processName.toLowerCase()); if (gameProcesses.length === 0) { @@ -60,13 +60,11 @@ export async function getPIDforProcessName(processName: string): Promise p.pid); } -export async function getPathFromProcess(pid: number){ - let pwsh_cmd = `(Get-Process -id ${pid}).Path` - - var {stdout, stderr} = await execFile('powershell', [ - pwsh_cmd - ]) - if (stderr){ +export async function getPathFromProcess(pid: number) { + const pwsh_cmd = `(Get-Process -id ${pid}).Path`; + + const { stdout, stderr } = await execFile('powershell', [pwsh_cmd]); + if (stderr) { return undefined; } return stdout; @@ -74,8 +72,8 @@ export async function getPathFromProcess(pid: number){ export async function getPIDsforFullPath(processPath: string): Promise> { const pidsList = await getPIDforProcessName(path.basename(processPath)); - let pids = pidsList.filter(async (pid) => { - return processPath === await getPathFromProcess(pid); + const pids = pidsList.filter(async (pid) => { + return processPath === (await getPathFromProcess(pid)); }); return pids; } @@ -166,24 +164,21 @@ export async function copyAndFillTemplate(srcPath: string, dstPath: string, valu return writeFile(dstPath, templStr); } -export interface EnvData{ +export interface EnvData { [key: string]: string; } +export async function getEnvFromProcess(pid: number) { + const pwsh_cmd = `(Get-Process -id ${pid}).StartInfo.EnvironmentVariables.ForEach( { $_.Key + "=" + $_.Value } )`; -export async function getEnvFromProcess(pid: number){ - let pwsh_cmd = `(Get-Process -id ${pid}).StartInfo.EnvironmentVariables.ForEach( { $_.Key + "=" + $_.Value } )` - - var {stdout, stderr} = await execFile('powershell', [ - pwsh_cmd - ]) - if (stderr){ + const { stdout, stderr } = await execFile('powershell', [pwsh_cmd]); + if (stderr) { return undefined; } - let otherEnv: EnvData = {} + const otherEnv: EnvData = {}; stdout.split('\r\n').forEach((line) => { - let [key, value] = line.split('='); - if (key && key !== ''){ + const [key, value] = line.split('='); + if (key && key !== '') { otherEnv[key] = value; } }); @@ -200,29 +195,29 @@ export async function CheckHash(data: Buffer, expectedHash: string) { return true; } -async function _GetHashOfFolder(folderPath: string, inputHash?: crypto.Hash): Promise{ +async function _GetHashOfFolder(folderPath: string, inputHash?: crypto.Hash): Promise { if (!inputHash) { return undefined; } - const info = await readdir(folderPath, {withFileTypes: true}); + const info = await readdir(folderPath, { withFileTypes: true }); if (!info || info.length == 0) { - return undefined; - } - for (let item of info) { - const fullPath = path.join(folderPath, item.name); - if (item.isFile()) { - const data = fs.readFileSync(fullPath); - inputHash.update(data); - } else if (item.isDirectory()) { - // recursively walk sub-folders - await _GetHashOfFolder(fullPath, inputHash); - } + return undefined; + } + for (const item of info) { + const fullPath = path.join(folderPath, item.name); + if (item.isFile()) { + const data = fs.readFileSync(fullPath); + inputHash.update(data); + } else if (item.isDirectory()) { + // recursively walk sub-folders + await _GetHashOfFolder(fullPath, inputHash); + } } return inputHash; } -export async function GetHashOfFolder(folderPath: string): Promise{ - return (await _GetHashOfFolder(folderPath, crypto.createHash('sha256')))?.digest('hex'); +export async function GetHashOfFolder(folderPath: string): Promise { + return (await _GetHashOfFolder(folderPath, crypto.createHash('sha256')))?.digest('hex'); } export async function CheckHashOfFolder(folderPath: string, expectedSHA256: string): Promise { @@ -230,15 +225,15 @@ export async function CheckHashOfFolder(folderPath: string, expectedSHA256: stri if (!hash) { return false; } - if (hash !== expectedSHA256){ - return false; + if (hash !== expectedSHA256) { + return false; } return true; } export async function CheckHashFile(filePath: string, expectedSHA256: string) { // get the hash of the file - if (!await exists(filePath) || !(await stat(filePath)).isFile()) { + if (!(await exists(filePath)) || !(await stat(filePath)).isFile()) { return false; } const buffer = await readFile(filePath); diff --git a/src/papyrus-lang-vscode/src/VsCodeUtilities.ts b/src/papyrus-lang-vscode/src/VsCodeUtilities.ts index 9f85b8eb..9894bfaa 100644 --- a/src/papyrus-lang-vscode/src/VsCodeUtilities.ts +++ b/src/papyrus-lang-vscode/src/VsCodeUtilities.ts @@ -1,7 +1,6 @@ import { CancellationTokenSource } from 'vscode'; import { delayAsync } from './Utilities'; - export async function waitWhile( func: () => Promise, cancellationToken = new CancellationTokenSource().token, diff --git a/src/papyrus-lang-vscode/src/WorkspaceGame.ts b/src/papyrus-lang-vscode/src/WorkspaceGame.ts index f6a45e0d..4d0d1c98 100644 --- a/src/papyrus-lang-vscode/src/WorkspaceGame.ts +++ b/src/papyrus-lang-vscode/src/WorkspaceGame.ts @@ -16,7 +16,7 @@ export async function getWorkspaceGameFromProjects(ppjFiles: Uri[]): Promise { return undefined; } - const ppjFiles: Uri[] = await workspace.findFiles(new RelativePattern(workspace.workspaceFolders[0], "**/*.ppj")); + const ppjFiles: Uri[] = await workspace.findFiles(new RelativePattern(workspace.workspaceFolders[0], '**/*.ppj')); return getWorkspaceGameFromProjects(ppjFiles); -} \ No newline at end of file +} diff --git a/src/papyrus-lang-vscode/src/common/GameHelpers.ts b/src/papyrus-lang-vscode/src/common/GameHelpers.ts index b02c241c..4c55baeb 100644 --- a/src/papyrus-lang-vscode/src/common/GameHelpers.ts +++ b/src/papyrus-lang-vscode/src/common/GameHelpers.ts @@ -1,191 +1,201 @@ -import path from "path"; -import { getRegistryKeyForGame, PapyrusGame, GameVariant, GetUserGameFolderName, getScriptExtenderName } from "../PapyrusGame"; -import * as fs from "fs"; -import { promisify } from "util"; -import { AddressLibAssetSuffix, AddressLibraryF4SEModName, AddressLibraryName, AddressLibrarySKSEAEModName, AddressLibrarySKSEModName } from "./constants"; -import { INIData } from "./INIHelpers"; -import { getHomeFolder, getRegistryValueData } from "./OSHelpers"; +import path from 'path'; +import { + getRegistryKeyForGame, + PapyrusGame, + GameVariant, + GetUserGameFolderName, + getScriptExtenderName, +} from '../PapyrusGame'; +import * as fs from 'fs'; +import { promisify } from 'util'; +import { + AddressLibAssetSuffix, + AddressLibraryF4SEModName, + AddressLibraryName, + AddressLibrarySKSEAEModName, + AddressLibrarySKSEModName, +} from './constants'; +import { INIData } from './INIHelpers'; +import { getHomeFolder, getRegistryValueData } from './OSHelpers'; const exists = promisify(fs.exists); const readdir = promisify(fs.readdir); const readFile = promisify(fs.readFile); export function getAsssetLibraryDLSuffix(addlibname: AddressLibraryName): AddressLibAssetSuffix { - switch (addlibname) { - case AddressLibrarySKSEModName: - return AddressLibAssetSuffix.SkyrimSE; - case AddressLibrarySKSEAEModName: - return AddressLibAssetSuffix.SkyrimAE; - case AddressLibraryF4SEModName: - return AddressLibAssetSuffix.Fallout4 - } + switch (addlibname) { + case AddressLibrarySKSEModName: + return AddressLibAssetSuffix.SkyrimSE; + case AddressLibrarySKSEAEModName: + return AddressLibAssetSuffix.SkyrimAE; + case AddressLibraryF4SEModName: + return AddressLibAssetSuffix.Fallout4; + } } export function getAddressLibNameFromAssetSuffix(suffix: AddressLibAssetSuffix): AddressLibraryName { - switch (suffix) { - case AddressLibAssetSuffix.SkyrimSE: - return AddressLibrarySKSEModName; - case AddressLibAssetSuffix.SkyrimAE: - return AddressLibrarySKSEAEModName; - case AddressLibAssetSuffix.Fallout4: - return AddressLibraryF4SEModName; - } + switch (suffix) { + case AddressLibAssetSuffix.SkyrimSE: + return AddressLibrarySKSEModName; + case AddressLibAssetSuffix.SkyrimAE: + return AddressLibrarySKSEAEModName; + case AddressLibAssetSuffix.Fallout4: + return AddressLibraryF4SEModName; + } } export function getAddressLibNames(game: PapyrusGame): AddressLibraryName[] { - if (game === PapyrusGame.fallout4) { - return [AddressLibraryF4SEModName]; - } else if (game === PapyrusGame.skyrimSpecialEdition) { - return [AddressLibrarySKSEModName, AddressLibrarySKSEAEModName]; - } - // there is no skyrim classic address library - return []; + if (game === PapyrusGame.fallout4) { + return [AddressLibraryF4SEModName]; + } else if (game === PapyrusGame.skyrimSpecialEdition) { + return [AddressLibrarySKSEModName, AddressLibrarySKSEAEModName]; + } + // there is no skyrim classic address library + return []; } export function CheckIfDebuggingIsEnabledInIni(iniData: INIData) { - return ( - iniData.Papyrus.bLoadDebugInformation === 1 && - iniData.Papyrus.bEnableTrace === 1 && - iniData.Papyrus.bEnableLogging === 1 - ); + return ( + iniData.Papyrus.bLoadDebugInformation === 1 && + iniData.Papyrus.bEnableTrace === 1 && + iniData.Papyrus.bEnableLogging === 1 + ); } export function TurnOnDebuggingInIni(skyrimIni: INIData) { - const _ini = structuredClone(skyrimIni); - _ini.Papyrus.bLoadDebugInformation = 1; - _ini.Papyrus.bEnableTrace = 1; - _ini.Papyrus.bEnableLogging = 1; - return _ini; + const _ini = structuredClone(skyrimIni); + _ini.Papyrus.bLoadDebugInformation = 1; + _ini.Papyrus.bEnableTrace = 1; + _ini.Papyrus.bEnableLogging = 1; + return _ini; } export async function FindUserGamePath(game: PapyrusGame, variant: GameVariant): Promise { - let GameFolderName: string = GetUserGameFolderName(game, variant); - let home = getHomeFolder(); - if (!home) { - return null; - } - let userGamePath = path.join(home, 'Documents', 'My Games', GameFolderName); - if (await exists(userGamePath)) { - return userGamePath; - } - return null; + const GameFolderName: string = GetUserGameFolderName(game, variant); + const home = getHomeFolder(); + if (!home) { + return null; + } + const userGamePath = path.join(home, 'Documents', 'My Games', GameFolderName); + if (await exists(userGamePath)) { + return userGamePath; + } + return null; } /** * We need to determine variants for things like the save game path * @param game - * @param installPath - * @returns + * @param installPath + * @returns */ export async function DetermineGameVariant(game: PapyrusGame, installPath: string): Promise { - // only Skyrim SE has variants, the rest are only sold on steam - if (game !== PapyrusGame.skyrimSpecialEdition){ + // only Skyrim SE has variants, the rest are only sold on steam + if (game !== PapyrusGame.skyrimSpecialEdition) { + return GameVariant.Steam; + } + if (!installPath || !(await exists(installPath))) { + // just default to steam + return GameVariant.Steam; + } + const gog_dll = path.join(installPath, 'Galaxy64.dll'); + const epic_dll = path.join(installPath, 'EOSSDK-Win64-Shipping.dll'); + if (await exists(gog_dll)) { + return GameVariant.GOG; + } + if (await exists(epic_dll)) { + return GameVariant.Epic; + } + // default to steam return GameVariant.Steam; - } - if (!installPath || !(await exists(installPath))) { - // just default to steam - return GameVariant.Steam - } - const gog_dll = path.join(installPath, 'Galaxy64.dll'); - const epic_dll = path.join(installPath, 'EOSSDK-Win64-Shipping.dll'); - if (await exists(gog_dll)) { - return GameVariant.GOG; - } - if (await exists(epic_dll)) { - return GameVariant.Epic; - } - // default to steam - return GameVariant.Steam; } async function findSkyrimSEEpic(): Promise { - const key = `\\SOFTWARE\\${process.arch === 'x64' ? 'WOW6432Node\\' : ''}Epic Games\\EpicGamesLauncher`; - const val = 'AppDataPath'; - let epicAppdatapath = await getRegistryValueData(key, val); - let manifestsDir: string; - if (epicAppdatapath) { - manifestsDir = path.join(epicAppdatapath, 'Manifests'); - } else if (process.env.PROGRAMDATA) { - // if the local app data path isn't set, try the global one - manifestsDir = path.join(process.env.PROGRAMDATA, 'Epic', 'EpicGamesLauncher', 'Data', 'Manifests'); - } else { - return null; - } - if (await exists(manifestsDir)) { - // list the directory and find the manifest for Skyrim SE - const manifestFiles = await readdir(manifestsDir); - for (const manifestFile of manifestFiles) { - // read the manifest file and check if it's for Skyrim SE - if (path.extname(manifestFile) !== '.item') { - continue; - } - let data = await readFile(path.join(manifestsDir, manifestFile), 'utf8'); - if (data) { - let manifest = JSON.parse(data); - if ( - manifest && - manifest.AppName && - (manifest.AppName === 'ac82db5035584c7f8a2c548d98c86b2c' || - manifest.AppName === '5d600e4f59974aeba0259c7734134e27') - ) { - if (manifest.InstallLocation && (await exists(manifest.InstallLocation))) { - return manifest.InstallLocation; - } - } - } - } - } - return null; + const key = `\\SOFTWARE\\${process.arch === 'x64' ? 'WOW6432Node\\' : ''}Epic Games\\EpicGamesLauncher`; + const val = 'AppDataPath'; + const epicAppdatapath = await getRegistryValueData(key, val); + let manifestsDir: string; + if (epicAppdatapath) { + manifestsDir = path.join(epicAppdatapath, 'Manifests'); + } else if (process.env.PROGRAMDATA) { + // if the local app data path isn't set, try the global one + manifestsDir = path.join(process.env.PROGRAMDATA, 'Epic', 'EpicGamesLauncher', 'Data', 'Manifests'); + } else { + return null; + } + if (await exists(manifestsDir)) { + // list the directory and find the manifest for Skyrim SE + const manifestFiles = await readdir(manifestsDir); + for (const manifestFile of manifestFiles) { + // read the manifest file and check if it's for Skyrim SE + if (path.extname(manifestFile) !== '.item') { + continue; + } + const data = await readFile(path.join(manifestsDir, manifestFile), 'utf8'); + if (data) { + const manifest = JSON.parse(data); + if ( + manifest && + manifest.AppName && + (manifest.AppName === 'ac82db5035584c7f8a2c548d98c86b2c' || + manifest.AppName === '5d600e4f59974aeba0259c7734134e27') + ) { + if (manifest.InstallLocation && (await exists(manifest.InstallLocation))) { + return manifest.InstallLocation; + } + } + } + } + } + return null; } async function findSkyrimSEGOG(): Promise { - const keynames = [ - // check Skyrim AE first - `\\SOFTWARE\\${process.arch === 'x64' ? 'WOW6432Node\\' : ''}GOG.com\\Games\\1162721350`, - // If AE isn't installed, check Skyrim SE - `\\SOFTWARE\\${process.arch === 'x64' ? 'WOW6432Node\\' : ''}GOG.com\\Games\\1711230643`, - ]; - for (const key of keynames) { - let gogpath = await getRegistryValueData(key, 'path'); - if (gogpath && (await exists(gogpath))) { - return gogpath; - } - } - return null; + const keynames = [ + // check Skyrim AE first + `\\SOFTWARE\\${process.arch === 'x64' ? 'WOW6432Node\\' : ''}GOG.com\\Games\\1162721350`, + // If AE isn't installed, check Skyrim SE + `\\SOFTWARE\\${process.arch === 'x64' ? 'WOW6432Node\\' : ''}GOG.com\\Games\\1711230643`, + ]; + for (const key of keynames) { + const gogpath = await getRegistryValueData(key, 'path'); + if (gogpath && (await exists(gogpath))) { + return gogpath; + } + } + return null; } async function FindGameSteamPath(game: PapyrusGame): Promise { - const key = `\\SOFTWARE\\${ - process.arch === 'x64' ? 'WOW6432Node\\' : '' - }Bethesda Softworks\\${getRegistryKeyForGame(game)}`; - const val = 'installed path'; - const pathValue = await getRegistryValueData(key, val); - if (pathValue && (await exists(pathValue))) { - return pathValue; - } - return null; + const key = `\\SOFTWARE\\${ + process.arch === 'x64' ? 'WOW6432Node\\' : '' + }Bethesda Softworks\\${getRegistryKeyForGame(game)}`; + const val = 'installed path'; + const pathValue = await getRegistryValueData(key, val); + if (pathValue && (await exists(pathValue))) { + return pathValue; + } + return null; } -export async function FindGamePath(game: PapyrusGame) -{ - if (game === PapyrusGame.fallout4 || game === PapyrusGame.skyrim) { - return FindGameSteamPath(game); - } else if (game === PapyrusGame.skyrimSpecialEdition) { - let path = await FindGameSteamPath(game); - if (path) { - return path; - } - path = await findSkyrimSEGOG(); - if (path) { - return path; - } - path = await findSkyrimSEEpic(); - if (path) { - return path; - } - } - return null; - +export async function FindGamePath(game: PapyrusGame) { + if (game === PapyrusGame.fallout4 || game === PapyrusGame.skyrim) { + return FindGameSteamPath(game); + } else if (game === PapyrusGame.skyrimSpecialEdition) { + let path = await FindGameSteamPath(game); + if (path) { + return path; + } + path = await findSkyrimSEGOG(); + if (path) { + return path; + } + path = await findSkyrimSEEpic(); + if (path) { + return path; + } + } + return null; } /** @@ -196,5 +206,5 @@ export async function FindGamePath(game: PapyrusGame) * @returns */ export function getRelativePluginPath(game: PapyrusGame) { - return `${getScriptExtenderName(game)}/Plugins`; + return `${getScriptExtenderName(game)}/Plugins`; } diff --git a/src/papyrus-lang-vscode/src/common/GithubHelpers.ts b/src/papyrus-lang-vscode/src/common/GithubHelpers.ts index d48d8f24..c7fc389d 100644 --- a/src/papyrus-lang-vscode/src/common/GithubHelpers.ts +++ b/src/papyrus-lang-vscode/src/common/GithubHelpers.ts @@ -1,5 +1,5 @@ import { getReleases } from '@terascope/fetch-github-release/dist/src/getReleases'; -import { GithubRelease, GithubReleaseAsset } from '@terascope/fetch-github-release/dist/src/interfaces'; +import { GithubRelease } from '@terascope/fetch-github-release/dist/src/interfaces'; import { downloadRelease } from '@terascope/fetch-github-release/dist/src/downloadRelease'; import { getLatest } from '@terascope/fetch-github-release/dist/src/getLatest'; import * as fs from 'fs'; @@ -19,7 +19,6 @@ export enum DownloadResult { cancelled, } - /** * Downloads all assets from a specific release * @param githubUserName The name of the user or organization that owns the repo @@ -30,13 +29,25 @@ export enum DownloadResult { * @throws An error if the repo does not exist or the release does not exist * @throws An error if the release has multiple assets with the same name * @throws An error if the download fails -*/ -export async function downloadAssetsFromGitHub(githubUserName: string, repoName: string, releaseId: number, downloadFolder: string): Promise { - const paths = await downloadRelease(githubUserName, repoName, downloadFolder, (release) => release.id == releaseId, undefined, true); - if (!paths || paths.length == 0){ - return undefined; - } - return paths; + */ +export async function downloadAssetsFromGitHub( + githubUserName: string, + repoName: string, + releaseId: number, + downloadFolder: string +): Promise { + const paths = await downloadRelease( + githubUserName, + repoName, + downloadFolder, + (release) => release.id == releaseId, + undefined, + true + ); + if (!paths || paths.length == 0) { + return undefined; + } + return paths; } /** @@ -52,12 +63,25 @@ export async function downloadAssetsFromGitHub(githubUserName: string, repoName: * @throws An error if the download fails * @throws An error if the asset does not exist in the release */ -export async function downloadAssetFromGitHub(githubUserName: string, repoName: string, release_id: number, assetFileName: string, downloadFolder: string): Promise{ - const paths = await downloadRelease(githubUserName, repoName, downloadFolder, (release) => release.id == release_id, (asset) => asset.name === assetFileName, true); - return (paths && paths.length > 0) ? paths[0] : undefined; +export async function downloadAssetFromGitHub( + githubUserName: string, + repoName: string, + release_id: number, + assetFileName: string, + downloadFolder: string +): Promise { + const paths = await downloadRelease( + githubUserName, + repoName, + downloadFolder, + (release) => release.id == release_id, + (asset) => asset.name === assetFileName, + true + ); + return paths && paths.length > 0 ? paths[0] : undefined; } -/** +/** * Downloads a specific asset from a specific release and checks the hash * @param githubUserName The name of the user or organization that owns the repo * @param repoName The name of the repo @@ -81,7 +105,7 @@ export async function DownloadAssetAndCheckHash( } catch (e) { return DownloadResult.downloadFailure; } - if (!dlPath || !await exists(dlPath)) { + if (!dlPath || !(await exists(dlPath))) { return DownloadResult.downloadFailure; } if (!CheckHashFile(dlPath, expectedSha256Sum)) { @@ -91,12 +115,16 @@ export async function DownloadAssetAndCheckHash( return DownloadResult.success; } -export async function GetLatestReleaseFromRepo(githubUserName: string, repoName: string, prerelease: boolean = false): Promise { - // if pre-releases == false, filter out pre-releases - const releaseFilter = !prerelease ? (release: GithubRelease) => release.prerelease == false : undefined; - const latestRelease = await getLatest(await getReleases(githubUserName, repoName), releaseFilter); - if (!latestRelease) { - return undefined; - } - return latestRelease; -} \ No newline at end of file +export async function GetLatestReleaseFromRepo( + githubUserName: string, + repoName: string, + prerelease: boolean = false +): Promise { + // if pre-releases == false, filter out pre-releases + const releaseFilter = !prerelease ? (release: GithubRelease) => release.prerelease == false : undefined; + const latestRelease = await getLatest(await getReleases(githubUserName, repoName), releaseFilter); + if (!latestRelease) { + return undefined; + } + return latestRelease; +} diff --git a/src/papyrus-lang-vscode/src/common/INIHelpers.ts b/src/papyrus-lang-vscode/src/common/INIHelpers.ts index 7317f7a6..b7bc95d1 100644 --- a/src/papyrus-lang-vscode/src/common/INIHelpers.ts +++ b/src/papyrus-lang-vscode/src/common/INIHelpers.ts @@ -1,67 +1,66 @@ import * as ini from 'ini'; -import * as fs from "fs"; +import * as fs from 'fs'; import { promisify } from 'util'; const readFile = promisify(fs.readFile); - export interface INIData { - [key: string]: any; + [key: string]: any; } export function ParseIniArray(data: INIData): INIData[] | undefined { - if (!data || data.size === undefined || data.size === null) { - return undefined; - } - let array = new Array(); - if (data.size === 0) { - return array; - } - for (let i = 0; i < data.size; i++) { - array.push({} as INIData); - } - // Keys in INI arrays are in the format of 1\{key1}, 1\{key2}, 2\{key1}, 2\{key2}, etc. - let keys = Object.keys(data); - keys.forEach((key) => { - if (key !== 'size') { - const parts = key.split('\\'); - if (parts.length === 2) { - // INI arrays are 1-indexed - const index = parseInt(parts[0], 10) - 1; - const subKey = parts[1]; - array[index][subKey] = data[key]; - } + if (!data || data.size === undefined || data.size === null) { + return undefined; + } + const array = new Array(); + if (data.size === 0) { + return array; } - }); - return array; + for (let i = 0; i < data.size; i++) { + array.push({} as INIData); + } + // Keys in INI arrays are in the format of 1\{key1}, 1\{key2}, 2\{key1}, 2\{key2}, etc. + const keys = Object.keys(data); + keys.forEach((key) => { + if (key !== 'size') { + const parts = key.split('\\'); + if (parts.length === 2) { + // INI arrays are 1-indexed + const index = parseInt(parts[0], 10) - 1; + const subKey = parts[1]; + array[index][subKey] = data[key]; + } + } + }); + return array; } export function SerializeIniArray(data: INIData[]): INIData { - let iniData = {} as INIData; - iniData.size = data.length; - data.forEach((value, index) => { - Object.keys(value).forEach((key) => { - iniData[`${index + 1}\\${key}`] = value[key]; + const iniData = {} as INIData; + iniData.size = data.length; + data.forEach((value, index) => { + Object.keys(value).forEach((key) => { + iniData[`${index + 1}\\${key}`] = value[key]; + }); }); - }); - return iniData; + return iniData; } export async function ParseIniFile(IniPath: string): Promise { - if (!fs.existsSync(IniPath) || !fs.lstatSync(IniPath).isFile()) { - return undefined; - } - let IniText = await readFile(IniPath, 'utf-8'); - if (!IniText) { - return undefined; - } - return ini.parse(IniText) as INIData; + if (!fs.existsSync(IniPath) || !fs.lstatSync(IniPath).isFile()) { + return undefined; + } + const IniText = await readFile(IniPath, 'utf-8'); + if (!IniText) { + return undefined; + } + return ini.parse(IniText) as INIData; } export async function WriteChangesToIni(gameIniPath: string, skyrimIni: INIData) { - const file = fs.openSync(gameIniPath, 'w'); - if (!file) { + const file = fs.openSync(gameIniPath, 'w'); + if (!file) { + return false; + } + fs.writeFileSync(file, ini.stringify(skyrimIni)); return false; - } - fs.writeFileSync(file, ini.stringify(skyrimIni)); - return false; } diff --git a/src/papyrus-lang-vscode/src/common/MO2Lib.ts b/src/papyrus-lang-vscode/src/common/MO2Lib.ts index 0e9883c5..7666bdda 100644 --- a/src/papyrus-lang-vscode/src/common/MO2Lib.ts +++ b/src/papyrus-lang-vscode/src/common/MO2Lib.ts @@ -1,7 +1,7 @@ import { existsSync, openSync, readdirSync, readFileSync, writeFileSync } from 'fs'; import * as fs from 'fs'; import path from 'path'; -import { INIData, ParseIniArray, ParseIniFile, SerializeIniArray, WriteChangesToIni } from './INIHelpers'; +import { INIData, ParseIniArray, ParseIniFile, SerializeIniArray } from './INIHelpers'; import { getLocalAppDataFolder, getRegistryValueData } from './OSHelpers'; export enum ModEnabledState { @@ -65,7 +65,6 @@ export interface MO2InstanceInfo { // import a library that deals with nukpg version strings // import the semver library -import * as semver from 'semver'; export interface WinVerObject { major: number; @@ -82,11 +81,11 @@ export class WinVer implements WinVerObject { public readonly privateNum: number = 0; public readonly version: string = '0.0.0.0'; public static fromVersionString(version: string): WinVer { - let parts = version.split('.'); - let major = parseInt(parts[0]); - let minor = parseInt(parts[1]); - let build = parseInt(parts[2]); - let privateNum = parseInt(parts[3]); + const parts = version.split('.'); + const major = parseInt(parts[0]); + const minor = parseInt(parts[1]); + const build = parseInt(parts[2]); + const privateNum = parseInt(parts[3]); return new WinVer({ major, minor, build, privateNum, version }); } public static lessThan(a: WinVerObject, b: WinVerObject): boolean { @@ -157,7 +156,7 @@ export interface MO2Location { } export function GetGlobalMO2DataFolder(): string | undefined { - let appdata = getLocalAppDataFolder(); + const appdata = getLocalAppDataFolder(); if (appdata === undefined) { return undefined; } @@ -175,11 +174,11 @@ export async function IsMO2Portable(MO2EXEPath: string): Promise { export async function GetMO2EXELocations(gameId?: MO2LongGameID, ...additionalIds: MO2LongGameID[]): Promise { let possibleLocations: string[] = []; - let nxmHandlerIniPath = await FindNXMHandlerIniPath(); + const nxmHandlerIniPath = await FindNXMHandlerIniPath(); if (nxmHandlerIniPath === undefined) { return possibleLocations; } - let MO2NXMData = await ParseIniFile(nxmHandlerIniPath); + const MO2NXMData = await ParseIniFile(nxmHandlerIniPath); if (MO2NXMData === undefined) { return possibleLocations; } @@ -189,7 +188,7 @@ export async function GetMO2EXELocations(gameId?: MO2LongGameID, ...additionalId } function getIDFromNXMHandlerName(nxmName: string): MO2LongGameID | undefined { - let _nxmName = nxmName.toLowerCase().replace(/ /g, ''); + const _nxmName = nxmName.toLowerCase().replace(/ /g, ''); switch (_nxmName) { case 'skyrimse': case 'skyrimspecialedition': @@ -238,27 +237,27 @@ function GetMO2EXELocationsFromNXMHandlerData( if (!nxmData.handlers) { return exePaths; } - let handler_array = ParseIniArray(nxmData.handlers); + const handler_array = ParseIniArray(nxmData.handlers); if (!handler_array || handler_array.length === 0) { return exePaths; } for (const handler of handler_array) { - let executable: string | undefined = handler.executable; - let games: string | undefined = handler.games; + const executable: string | undefined = handler.executable; + const games: string | undefined = handler.games; if (!executable || !games) { continue; } - let gameList = NormalizeIniString(games) + const gameList = NormalizeIniString(games) .split(',') .filter((val) => val !== ''); if (!gameId) { exePaths.push(executable); } else { - let _args = [gameId, ...additionalIds]; + const _args = [gameId, ...additionalIds]; for (const gameID of _args) { if ( gameList.filter((val) => { - let _valID = getIDFromNXMHandlerName(val); + const _valID = getIDFromNXMHandlerName(val); if (_valID === undefined) { return false; } @@ -326,16 +325,16 @@ function _normMO2Path(pathstring: string | undefined, basedir: string | undefine } function ParseMO2CustomExecutable(iniData: INIData) { - let title = _normInistr(iniData.title); - let binary = _normIniPath(iniData.binary); - let steamAppID = _normInistr(iniData.steamAppID) || ''; - let toolbar = iniData.toolbar === true; // explicit boolean check in case unset - let hide = iniData.hide === true; - let ownicon = iniData.ownicon === true; - let arguments_ = _normInistr(iniData.arguments) || ''; - let workingDirectory = _normIniPath(iniData.workingDirectory) || ''; + const title = _normInistr(iniData.title); + const binary = _normIniPath(iniData.binary); + const steamAppID = _normInistr(iniData.steamAppID) || ''; + const toolbar = iniData.toolbar === true; // explicit boolean check in case unset + const hide = iniData.hide === true; + const ownicon = iniData.ownicon === true; + const arguments_ = _normInistr(iniData.arguments) || ''; + const workingDirectory = _normIniPath(iniData.workingDirectory) || ''; if (title !== undefined && binary !== undefined) { - let result: MO2CustomExecutableInfo = { + const result: MO2CustomExecutableInfo = { arguments: arguments_, binary: binary, hide: hide, @@ -351,9 +350,9 @@ function ParseMO2CustomExecutable(iniData: INIData) { } function ParseMO2CustomExecutables(iniArray: INIData[]) { - let result: MO2CustomExecutableInfo[] = []; + const result: MO2CustomExecutableInfo[] = []; for (const iniData of iniArray) { - let parsed = ParseMO2CustomExecutable(iniData); + const parsed = ParseMO2CustomExecutable(iniData); if (parsed) { result.push(parsed); } @@ -390,36 +389,36 @@ function ParseMO2CustomExecutables(iniArray: INIData[]) { * @returns */ function ParseInstanceINI(iniPath: string, iniData: INIData, isPortable: boolean): MO2InstanceInfo | undefined { - let iniBaseDir = NormalizePath(path.dirname(iniPath)); - let instanceName = isPortable ? 'portable' : path.basename(iniBaseDir); - let gameName: MO2LongGameID = iniData.General['gameName']; + const iniBaseDir = NormalizePath(path.dirname(iniPath)); + const instanceName = isPortable ? 'portable' : path.basename(iniBaseDir); + const gameName: MO2LongGameID = iniData.General['gameName']; if (gameName === undefined) { return undefined; } - let gameDirPath = _normIniPath(iniData.General['gamePath']); + const gameDirPath = _normIniPath(iniData.General['gamePath']); if (gameDirPath === undefined) { return undefined; } // TODO: We should probably pin to a specific minor version of MO2 - let version = iniData.General['version']; + const version = iniData.General['version']; // TODO: Figure out if this is ever not set - let selectedProfile = _normInistr(iniData.General['selected_profile']) || 'Default'; + const selectedProfile = _normInistr(iniData.General['selected_profile']) || 'Default'; - let settings = iniData.Settings || {}; // Settings may be empty; we don't need it to populate the rest of the information + const settings = iniData.Settings || {}; // Settings may be empty; we don't need it to populate the rest of the information - let baseDirectory = _normIniPath(settings['base_directory']) || iniBaseDir; - let downloadsPath = + const baseDirectory = _normIniPath(settings['base_directory']) || iniBaseDir; + const downloadsPath = _normMO2Path(settings['download_directory'], baseDirectory) || path.join(baseDirectory, 'downloads'); - let modsPath = _normMO2Path(settings['mod_directory'], baseDirectory) || path.join(baseDirectory, 'mods'); - let cachesPath = _normMO2Path(settings['cache_directory'], baseDirectory) || path.join(baseDirectory, 'webcache'); - let profilesPath = + const modsPath = _normMO2Path(settings['mod_directory'], baseDirectory) || path.join(baseDirectory, 'mods'); + const cachesPath = _normMO2Path(settings['cache_directory'], baseDirectory) || path.join(baseDirectory, 'webcache'); + const profilesPath = _normMO2Path(settings['profiles_directory'], baseDirectory) || path.join(baseDirectory, 'profiles'); - let overwritePath = + const overwritePath = _normMO2Path(settings['overwrite_directory'], baseDirectory) || path.join(baseDirectory, 'overwrite'); let customExecutables: MO2CustomExecutableInfo[] = []; if (iniData.customExecutables) { - let arr = ParseIniArray(iniData.customExecutables); + const arr = ParseIniArray(iniData.customExecutables); if (arr && arr.length > 0) { customExecutables = ParseMO2CustomExecutables(arr); } @@ -446,7 +445,7 @@ function ParseInstanceINI(iniPath: string, iniData: INIData, isPortable: boolean * @returns MO2InstanceInfo */ export async function GetMO2InstanceInfo(iniPath: string): Promise { - let iniData = await ParseIniFile(iniPath); + const iniData = await ParseIniFile(iniPath); if (iniData === undefined) { return undefined; } @@ -455,7 +454,7 @@ export async function GetMO2InstanceInfo(iniPath: string): Promise { // check that all the directory paths exist and they are directories - let dirPaths = [ + const dirPaths = [ info.gameDirPath, info.baseDirectory, info.downloadsFolder, @@ -464,7 +463,7 @@ export async function validateInstanceLocationInfo(info: MO2InstanceInfo): Promi info.profilesFolder, info.overwriteFolder, ]; - for (let p of dirPaths) { + for (const p of dirPaths) { if (!existsSync(p)) { return false; } @@ -483,10 +482,10 @@ export async function FindInstanceForEXE(MO2EXEPath: string, instanceName?: stri if (!fs.existsSync(MO2EXEPath)) { return undefined; } - let isPortable = await IsMO2Portable(MO2EXEPath); + const isPortable = await IsMO2Portable(MO2EXEPath); if (isPortable) { - let instanceFolder = path.dirname(MO2EXEPath); - let instanceIniPath = path.join(instanceFolder, InstanceIniName); + const instanceFolder = path.dirname(MO2EXEPath); + const instanceIniPath = path.join(instanceFolder, InstanceIniName); return await GetMO2InstanceInfo(instanceIniPath); } else if (instanceName !== undefined && instanceName !== 'portable') { return await FindGlobalInstance(instanceName); @@ -506,10 +505,10 @@ export async function GetLocationInfoForEXE( if (!fs.existsSync(MO2EXEPath)) { return undefined; } - let isPortable = await IsMO2Portable(MO2EXEPath); + const isPortable = await IsMO2Portable(MO2EXEPath); let instanceInfos: MO2InstanceInfo[] = []; if (isPortable) { - let instanceInfo = await GetMO2InstanceInfo(_portableExeIni(MO2EXEPath)); + const instanceInfo = await GetMO2InstanceInfo(_portableExeIni(MO2EXEPath)); instanceInfos = instanceInfo ? [instanceInfo] : []; } else { instanceInfos = await FindGlobalInstances(gameId, ...addtionalIds); @@ -528,15 +527,15 @@ export async function FindAllKnownMO2EXEandInstanceLocations( gameID?: MO2LongGameID, ...additionalIds: MO2LongGameID[] ): Promise { - let possibleLocations: MO2Location[] = []; - let exeLocations = await GetMO2EXELocations(gameID, ...additionalIds); + const possibleLocations: MO2Location[] = []; + const exeLocations = await GetMO2EXELocations(gameID, ...additionalIds); if (exeLocations.length !== 0) { - let globalInstances = (await FindGlobalInstances(gameID)) || []; - for (let exeLocation of exeLocations) { + const globalInstances = (await FindGlobalInstances(gameID)) || []; + for (const exeLocation of exeLocations) { let instanceInfos: MO2InstanceInfo[] | undefined = undefined; - let isPortable = await IsMO2Portable(exeLocation); + const isPortable = await IsMO2Portable(exeLocation); if (isPortable) { - let instanceInfo = await GetMO2InstanceInfo(_portableExeIni(exeLocation)); + const instanceInfo = await GetMO2InstanceInfo(_portableExeIni(exeLocation)); instanceInfos = instanceInfo ? [instanceInfo] : []; } else { instanceInfos = globalInstances; @@ -555,7 +554,7 @@ export async function FindAllKnownMO2EXEandInstanceLocations( } function GetNXMHandlerIniPath(): string | undefined { - let global = GetGlobalMO2DataFolder(); + const global = GetGlobalMO2DataFolder(); if (global === undefined) { return undefined; } @@ -563,7 +562,7 @@ function GetNXMHandlerIniPath(): string | undefined { } export async function FindNXMHandlerIniPath(): Promise { - let nxmHandlerIniPath = GetNXMHandlerIniPath(); + const nxmHandlerIniPath = GetNXMHandlerIniPath(); if (nxmHandlerIniPath === undefined || !existsSync(nxmHandlerIniPath)) { return undefined; } @@ -571,7 +570,7 @@ export async function FindNXMHandlerIniPath(): Promise { } export async function IsInstanceOfGame(gameID: MO2LongGameID, instanceIniPath: string): Promise { - let iniData = await ParseIniFile(instanceIniPath); + const iniData = await ParseIniFile(instanceIniPath); if (iniData === undefined) { return false; } @@ -587,8 +586,8 @@ function _isInstanceOfGames( return false; } - let gameIDs = [gameID, ...additionalIds]; - for (let id of gameIDs) { + const gameIDs = [gameID, ...additionalIds]; + for (const id of gameIDs) { if (instanceIniData.General.gameName === id) { return true; } @@ -597,17 +596,17 @@ function _isInstanceOfGames( } export async function FindGlobalInstance(name: string): Promise { - let globalFolder = GetGlobalMO2DataFolder(); + const globalFolder = GetGlobalMO2DataFolder(); if (globalFolder === undefined || (!existsSync(globalFolder) && !fs.statSync(globalFolder).isDirectory())) { return undefined; } - let instanceNames = readdirSync(globalFolder, { withFileTypes: true }); - let instance = instanceNames.find((dirent) => dirent.isDirectory() && dirent.name === name); + const instanceNames = readdirSync(globalFolder, { withFileTypes: true }); + const instance = instanceNames.find((dirent) => dirent.isDirectory() && dirent.name === name); if (instance === undefined) { return undefined; } - let instanceIniPath = path.join(globalFolder, instance.name, InstanceIniName); - let iniData = await ParseIniFile(instanceIniPath); + const instanceIniPath = path.join(globalFolder, instance.name, InstanceIniName); + const iniData = await ParseIniFile(instanceIniPath); if (!iniData) { return undefined; } @@ -618,24 +617,24 @@ export async function FindGlobalInstances( gameId?: MO2LongGameID, ...additionalIds: MO2LongGameID[] ): Promise { - let possibleLocations: MO2InstanceInfo[] = []; - let globalFolder = GetGlobalMO2DataFolder(); + const possibleLocations: MO2InstanceInfo[] = []; + const globalFolder = GetGlobalMO2DataFolder(); // list all the directories in globalMO2Data if (globalFolder === undefined || (!existsSync(globalFolder) && !fs.statSync(globalFolder).isDirectory())) { return []; } - let instanceNames = readdirSync(globalFolder, { withFileTypes: true }); - for (let dirent of instanceNames) { + const instanceNames = readdirSync(globalFolder, { withFileTypes: true }); + for (const dirent of instanceNames) { if (dirent.isDirectory()) { - let instanceIniPath = path.join(globalFolder, dirent.name, InstanceIniName); - let iniData = await ParseIniFile(instanceIniPath); + const instanceIniPath = path.join(globalFolder, dirent.name, InstanceIniName); + const iniData = await ParseIniFile(instanceIniPath); if (!iniData) { continue; } if (gameId !== undefined && !_isInstanceOfGames(iniData, gameId, ...additionalIds)) { continue; } - let info = ParseInstanceINI(instanceIniPath, iniData, false); + const info = ParseInstanceINI(instanceIniPath, iniData, false); if (info !== undefined) { possibleLocations.push(info); } @@ -646,7 +645,7 @@ export async function FindGlobalInstances( export async function GetCurrentGlobalInstance(): Promise { // HKEY_CURRENT_USER\SOFTWARE\Mod Organizer Team\Mod Organizer\CurrentInstance - let currentInstanceName = await getRegistryValueData( + const currentInstanceName = await getRegistryValueData( 'SOFTWARE\\Mod Organizer Team\\Mod Organizer', 'CurrentInstance', 'HKCU' @@ -687,14 +686,14 @@ export async function GetCurrentGlobalInstance(): Promise(); + const modlist = new Array(); const modlistLines = modListContents.replace(/\r\n/g, '\n').split('\n'); - for (let line of modlistLines) { + for (const line of modlistLines) { if (line.charAt(0) === '#' || line === '') { continue; } - let indic = line.charAt(0); - let modName = line.substring(1); + const indic = line.charAt(0); + const modName = line.substring(1); let modEnabledState: ModEnabledState | undefined = undefined; switch (indic) { case '+': @@ -730,9 +729,9 @@ export async function ParseModListFile(modlistPath: string): Promise, modName: * @returns */ - export function IndexOfModList(modlist: Array, modName: string) { return modlist.findIndex((m) => m.name === modName); } export function RemoveMod(modlist: Array, modName: string) { - let modIndex = modlist.findIndex((m) => m.name === modName); + const modIndex = modlist.findIndex((m) => m.name === modName); if (modIndex !== -1) { return modlist.slice(0, modIndex).concat(modlist.slice(modIndex + 1)); } @@ -762,7 +760,7 @@ export function RemoveMod(modlist: Array, modName: string) { export function AddModToBeginningOfModList(modlist: Array, mod: ModListItem) { // check if the mod is already in the modlist - let modIndex = modlist.findIndex((m) => m.name === mod.name); + const modIndex = modlist.findIndex((m) => m.name === mod.name); if (modIndex !== -1) { // if the mod is already in the modlist, remove it and return the modlist with the specified mod at the top return [mod].concat(modlist.slice(0, modIndex).concat(modlist.slice(modIndex + 1))); @@ -772,7 +770,7 @@ export function AddModToBeginningOfModList(modlist: Array, mod: Mod export function AddModIfNotInModList(modlist: Array, mod: ModListItem) { // check if the mod is already in the modlist - let modIndex = IndexOfModList(modlist, mod.name); + const modIndex = IndexOfModList(modlist, mod.name); if (modIndex === -1) { // if the mod is not already in the modlist, add it at the beginning return [mod].concat(modlist); @@ -782,11 +780,11 @@ export function AddModIfNotInModList(modlist: Array, mod: ModListIt } export function AddOrEnableModInModList(modlist: Array, modName: string) { - let modIndex = modlist.findIndex((m) => m.name === modName); + const modIndex = modlist.findIndex((m) => m.name === modName); if (modIndex !== -1) { - return modlist.slice(0, modIndex).concat( - new ModListItem(modName, ModEnabledState.enabled), - modlist.slice(modIndex + 1)); + return modlist + .slice(0, modIndex) + .concat(new ModListItem(modName, ModEnabledState.enabled), modlist.slice(modIndex + 1)); } return AddModToBeginningOfModList(modlist, new ModListItem(modName, ModEnabledState.enabled)); } @@ -794,14 +792,14 @@ export function AddOrEnableModInModList(modlist: Array, modName: st // modlist.txt has to be in CRLF, because MO2 is cursed export function ModListToText(modlist: Array) { let modlistText = '# This file was automatically generated by Mod Organizer.\r\n'; - for (let mod of modlist) { + for (const mod of modlist) { modlistText += mod.enabled + mod.name + '\r\n'; } return modlistText; } export function WriteChangesToModListFile(modlistPath: string, modlist: Array) { - let modlistContents = ModListToText(modlist); + const modlistContents = ModListToText(modlist); fs.rmSync(modlistPath, { force: true }); if (!openSync(modlistPath, 'w')) { return false; @@ -819,7 +817,7 @@ export function WriteChangesToModListFile(modlistPath: string, modlist: Array( - key: string | number | symbol, - obj: T, - ): key is keyof T { +export function isKeyOfObject(key: string | number | symbol, obj: T): key is keyof T { return key in obj; - } - +} + function ParseModMetaIni(modMetaIni: INIData): MO2ModMeta | undefined { if (!modMetaIni) { return undefined; @@ -932,23 +927,23 @@ function ParseModMetaIni(modMetaIni: INIData): MO2ModMeta | undefined { ) { return undefined; } - let general = modMetaIni.General; + const general = modMetaIni.General; // check if each key in general is a key in the type MO2ModMeta - let modMeta = {} as any; - for (let key in general) { + const modMeta = {} as any; + for (const key in general) { if (isKeyOfObject(key, general as MO2ModMeta)) { modMeta[key] = general[key]; } } - let installedFilesSize = modMetaIni.installedFiles.size; + const installedFilesSize = modMetaIni.installedFiles.size; if (!installedFilesSize) { return undefined; } - let installedFiles = ParseIniArray(modMetaIni.installedFiles); + const installedFiles = ParseIniArray(modMetaIni.installedFiles); if (!installedFiles) { return undefined; } - modMeta["installedFiles"] = installedFiles.map((installedFile) => { + modMeta['installedFiles'] = installedFiles.map((installedFile) => { return { modid: installedFile.modid, fileid: installedFile.fileid, @@ -958,11 +953,11 @@ function ParseModMetaIni(modMetaIni: INIData): MO2ModMeta | undefined { } export function SerializeModMetaInfo(info: MO2ModMeta) { - let ini = {} as INIData; + const ini = {} as INIData; ini.General = {} as INIData; Object.keys(info).forEach((key) => { if (key !== 'installedFiles' && info[key as keyof MO2ModMeta] !== undefined) { - ini.General[key] = info[key as keyof MO2ModMeta]; + ini.General[key] = info[key as keyof MO2ModMeta]; } }); ini.installedFiles = SerializeIniArray(info.installedFiles); @@ -970,7 +965,7 @@ export function SerializeModMetaInfo(info: MO2ModMeta) { } export async function ParseModMetaIniFile(modMetaIniPath: string) { - let modMetaIni = await ParseIniFile(modMetaIniPath); + const modMetaIni = await ParseIniFile(modMetaIniPath); if (!modMetaIni) { return undefined; } @@ -978,5 +973,5 @@ export async function ParseModMetaIniFile(modMetaIniPath: string) { } export function AddSeparatorToBeginningOfModList(name: string, modList: ModListItem[]): ModListItem[] { - return AddModIfNotInModList(modList, new ModListItem(name + "_separator", ModEnabledState.disabled)) + return AddModIfNotInModList(modList, new ModListItem(name + '_separator', ModEnabledState.disabled)); } diff --git a/src/papyrus-lang-vscode/src/common/OSHelpers.ts b/src/papyrus-lang-vscode/src/common/OSHelpers.ts index cee77d2f..ead30e64 100644 --- a/src/papyrus-lang-vscode/src/common/OSHelpers.ts +++ b/src/papyrus-lang-vscode/src/common/OSHelpers.ts @@ -1,26 +1,26 @@ import { promisify } from 'util'; import winreg from 'winreg'; -export function getLocalAppDataFolder(){ - return process.env.LOCALAPPDATA; +export function getLocalAppDataFolder() { + return process.env.LOCALAPPDATA; } export function getHomeFolder() { - return process.env.HOMEPATH; + return process.env.HOMEPATH; } -export function getUserName(){ - return process.env.USERNAME; +export function getUserName() { + return process.env.USERNAME; } -export function getTempFolder(){ - return process.env.TEMP; +export function getTempFolder() { + return process.env.TEMP; } export async function getRegistryValueData(key: string, value: string, hive: string = 'HKLM') { - let reg = new winreg({ - hive, - key, - }); - try { - const item = await promisify(reg.get).call(reg, value); - return item.value; - } catch (e) {} - return null; + const reg = new winreg({ + hive, + key, + }); + try { + const item = await promisify(reg.get).call(reg, value); + return item.value; + } catch (e) {} + return null; } diff --git a/src/papyrus-lang-vscode/src/common/PathResolver.ts b/src/papyrus-lang-vscode/src/common/PathResolver.ts index d47571c7..455d389a 100644 --- a/src/papyrus-lang-vscode/src/common/PathResolver.ts +++ b/src/papyrus-lang-vscode/src/common/PathResolver.ts @@ -78,9 +78,9 @@ export class PathResolver implements IPathResolver { /************************************************************************* */ public async getDebugPluginBundledPath(game: PapyrusGame) { - let dll = getPluginDllName(game); - if (!dll){ - throw new Error("Debugging not supported for game " + game); + const dll = getPluginDllName(game); + if (!dll) { + throw new Error('Debugging not supported for game ' + game); } return this._asExtensionAbsolutePath(path.join(bundledPluginPath, dll)); } @@ -205,7 +205,7 @@ export function getPluginDllName(game: PapyrusGame, legacy = false) { case PapyrusGame.skyrimSpecialEdition: return 'DarkId.Papyrus.DebugServer.Skyrim.dll'; default: - throw new Error("Debugging not supported for game " + game); + throw new Error('Debugging not supported for game ' + game); } } @@ -249,7 +249,7 @@ async function resolveUserGamePath( installPath: string, context: ExtensionContext ): Promise { - let _installPath : string | null = installPath; + let _installPath: string | null = installPath; if (!(await exists(installPath))) { _installPath = await resolveInstallPath(game, installPath, context); } diff --git a/src/papyrus-lang-vscode/src/common/constants.ts b/src/papyrus-lang-vscode/src/common/constants.ts index b59426e4..451a529c 100644 --- a/src/papyrus-lang-vscode/src/common/constants.ts +++ b/src/papyrus-lang-vscode/src/common/constants.ts @@ -5,15 +5,18 @@ export const extensionId = 'papyrus-lang-vscode'; export const extensionQualifiedId = `joelday.${extensionId}`; export enum GlobalState { - PapyrusVersion = 'papyrusVersion' + PapyrusVersion = 'papyrusVersion', } -export const PDSModName = "Papyrus Debug Extension"; -export const AddressLibraryF4SEModName = "Address Library for F4SE Plugins"; -export const AddressLibrarySKSEAEModName = "Address Library for SKSE Plugins (AE)"; -export const AddressLibrarySKSEModName = "Address Library for SKSE Plugins"; +export const PDSModName = 'Papyrus Debug Extension'; +export const AddressLibraryF4SEModName = 'Address Library for F4SE Plugins'; +export const AddressLibrarySKSEAEModName = 'Address Library for SKSE Plugins (AE)'; +export const AddressLibrarySKSEModName = 'Address Library for SKSE Plugins'; // TODO: Move these elsewhere -export type AddressLibraryName = typeof AddressLibraryF4SEModName | typeof AddressLibrarySKSEAEModName | typeof AddressLibrarySKSEModName; +export type AddressLibraryName = + | typeof AddressLibraryF4SEModName + | typeof AddressLibrarySKSEAEModName + | typeof AddressLibrarySKSEModName; export enum AddressLibAssetSuffix { SkyrimSE = 'SkyrimSE', SkyrimAE = 'SkyrimAE', diff --git a/src/papyrus-lang-vscode/src/debugger/AddLibHelpers.ts b/src/papyrus-lang-vscode/src/debugger/AddLibHelpers.ts index 373685c9..5bd10880 100644 --- a/src/papyrus-lang-vscode/src/debugger/AddLibHelpers.ts +++ b/src/papyrus-lang-vscode/src/debugger/AddLibHelpers.ts @@ -64,9 +64,9 @@ export function GetAssetZipForSuffixFromRelease( export function getAssetListFromAddLibRelease(release: GithubRelease): AddressLibReleaseAssetList | undefined { let assetZip: string | undefined | Error; - let ret: AddressLibReleaseAssetList = new Object() as AddressLibReleaseAssetList; + const ret: AddressLibReleaseAssetList = new Object() as AddressLibReleaseAssetList; ret.version = release.tag_name; - for (let idx in AddressLibAssetSuffix) { + for (const idx in AddressLibAssetSuffix) { const assetSuffix: AddressLibAssetSuffix = AddressLibAssetSuffix[idx as keyof typeof AddressLibAssetSuffix]; assetZip = GetAssetZipForSuffixFromRelease(release, assetSuffix); if (!assetZip) { @@ -137,7 +137,7 @@ export async function DownloadLatestAddressLibs( const sha256Sums = JSON.parse(sha256buf); const retryLimit = 3; let retries = 0; - for (let idx in AddressLibAssetSuffix) { + for (const idx in AddressLibAssetSuffix) { const assetSuffix: AddressLibAssetSuffix = AddressLibAssetSuffix[idx as keyof typeof AddressLibAssetSuffix]; if (cancellationToken.isCancellationRequested) return DownloadResult.cancelled; @@ -184,7 +184,7 @@ export async function DownloadLatestAddressLibs( if (!(await _checkAddlibExtracted(asset.folderName, downloadFolder))) { return DownloadResult.filesystemFailure; } - let folderHash = await GetHashOfFolder(ExtractedFolderPath); + const folderHash = await GetHashOfFolder(ExtractedFolderPath); if (!folderHash) { return DownloadResult.filesystemFailure; } @@ -214,7 +214,7 @@ export async function GetAssetList(jsonPath: string) { return undefined; } // check integrity - for (let idx in AddressLibAssetSuffix) { + for (const idx in AddressLibAssetSuffix) { const assetSuffix: AddressLibAssetSuffix = AddressLibAssetSuffix[idx as keyof typeof AddressLibAssetSuffix]; const currentAsset = _getAsset(assetList, assetSuffix); if (!currentAsset) { @@ -241,7 +241,7 @@ export async function _checkDownloadIntegrity( if (!assetList) { return false; } - for (let idx in AddressLibAssetSuffix) { + for (const idx in AddressLibAssetSuffix) { const assetSuffix: AddressLibAssetSuffix = AddressLibAssetSuffix[idx as keyof typeof AddressLibAssetSuffix]; const currentAsset = _getAsset(assetList, assetSuffix); if (!currentAsset) { @@ -270,7 +270,7 @@ export async function _checkAddlibExtracted(name: AddressLibraryName, modsDir: s // TODO: refactor this const SEDIR = name.indexOf('SKSE') >= 0 ? 'SKSE' : 'F4SE'; const pluginsdir = path.join(addressLibInstallPath, SEDIR, 'Plugins'); - + if (!fs.existsSync(pluginsdir) || !fs.lstatSync(pluginsdir).isDirectory()) { return false; } @@ -334,7 +334,7 @@ export async function _checkAddressLibsInstalled( assetList?: AddressLibReleaseAssetList ): Promise { const addressLibFolderNames = getAddressLibNames(game); - for (let name of addressLibFolderNames) { + for (const name of addressLibFolderNames) { const state = await _checkAddressLibInstalled(name, modsDir, assetList); if (state !== AddressLibInstalledState.installed) { return state; @@ -354,7 +354,7 @@ export async function _installAddressLibs( cancellationToken: CancellationToken ): Promise { const addressLibNames = getAddressLibNames(game); - for (let name of addressLibNames) { + for (const name of addressLibNames) { if (cancellationToken.isCancellationRequested) { return false; } diff --git a/src/papyrus-lang-vscode/src/debugger/AddressLibInstallService.ts b/src/papyrus-lang-vscode/src/debugger/AddressLibInstallService.ts index 00f35048..768eeae3 100644 --- a/src/papyrus-lang-vscode/src/debugger/AddressLibInstallService.ts +++ b/src/papyrus-lang-vscode/src/debugger/AddressLibInstallService.ts @@ -1,12 +1,10 @@ import { inject, injectable, interfaces } from 'inversify'; import { IPathResolver } from '../common/PathResolver'; import { PapyrusGame } from '../PapyrusGame'; -import { - DownloadResult, -} from '../common/GithubHelpers'; +import { DownloadResult } from '../common/GithubHelpers'; import { CancellationToken, CancellationTokenSource } from 'vscode'; -import * as AddLib from './AddLibHelpers' +import * as AddLib from './AddLibHelpers'; export enum AddressLibDownloadedState { notDownloaded, @@ -38,16 +36,16 @@ export interface IAddressLibraryInstallService { export class AddressLibraryInstallService implements IAddressLibraryInstallService { private readonly _pathResolver: IPathResolver; - constructor( - @inject(IPathResolver) pathResolver: IPathResolver - ) { + constructor(@inject(IPathResolver) pathResolver: IPathResolver) { this._pathResolver = pathResolver; } - public async DownloadLatestAddressLibs(cancellationToken = new CancellationTokenSource().token): Promise { + public async DownloadLatestAddressLibs( + cancellationToken = new CancellationTokenSource().token + ): Promise { const addressLibDownloadPath = await this._pathResolver.getAddressLibraryDownloadFolder(); const addressLibDLJSONPath = await this._pathResolver.getAddressLibraryDownloadJSON(); - let status = await AddLib.DownloadLatestAddressLibs( + const status = await AddLib.DownloadLatestAddressLibs( addressLibDownloadPath, addressLibDLJSONPath, cancellationToken @@ -101,7 +99,6 @@ export class AddressLibraryInstallService implements IAddressLibraryInstallServi return AddressLibDownloadedState.latest; } - /** * Right now, this just checks if the address libraries are installed or not * It returns either "Installed" or "Not Installed". @@ -148,7 +145,7 @@ export class AddressLibraryInstallService implements IAddressLibraryInstallServi return false; } const addressLibDownloadPath = await this._pathResolver.getAddressLibraryDownloadFolder(); - let downloadedState = await this.getDownloadedState(); + const downloadedState = await this.getDownloadedState(); if (downloadedState === AddressLibDownloadedState.notDownloaded) { if (forceDownload) { if ((await this.DownloadLatestAddressLibs(cancellationToken)) != DownloadResult.success) { diff --git a/src/papyrus-lang-vscode/src/debugger/DebugLauncherService.ts b/src/papyrus-lang-vscode/src/debugger/DebugLauncherService.ts index 98e41ed7..aebbae97 100644 --- a/src/papyrus-lang-vscode/src/debugger/DebugLauncherService.ts +++ b/src/papyrus-lang-vscode/src/debugger/DebugLauncherService.ts @@ -4,15 +4,12 @@ import { CancellationToken, CancellationTokenSource, window } from 'vscode'; import { IPathResolver } from '../common/PathResolver'; import { PapyrusGame } from '../PapyrusGame'; import { ILanguageClientManager } from '../server/LanguageClientManager'; -import { getGameIsRunning, getGamePIDs, mkdirIfNeeded } from '../Utilities'; +import { getGameIsRunning, getGamePIDs } from '../Utilities'; -import * as path from 'path'; import * as fs from 'fs'; import { promisify } from 'util'; -import md5File from 'md5-file'; import { ChildProcess, spawn } from 'node:child_process'; -import { timer } from 'rxjs'; import { execFile as _execFile } from 'child_process'; const execFile = promisify(_execFile); @@ -107,8 +104,8 @@ export class DebugLauncherService implements IDebugLauncherService { cancellationToken = this.cancellationTokenSource.token; } this.currentGame = game; - let cmd = launcherCommand.command; - let args = launcherCommand.args; + const cmd = launcherCommand.command; + const args = launcherCommand.args; let _stdOut: string = ''; let _stdErr: string = ''; this.launcherProcess = spawn(cmd, args); @@ -151,7 +148,7 @@ export class DebugLauncherService implements IDebugLauncherService { } // we can't get the PID of the game from the launcher process because // both MO2 and the script extender loaders fork and deatch the game process - let gamePIDs = await getGamePIDs(game); + const gamePIDs = await getGamePIDs(game); if (gamePIDs.length === 0) { return DebugLaunchState.gameFailedToStart; diff --git a/src/papyrus-lang-vscode/src/debugger/DebugSupportInstallService.ts b/src/papyrus-lang-vscode/src/debugger/DebugSupportInstallService.ts index be769b5d..c7454919 100644 --- a/src/papyrus-lang-vscode/src/debugger/DebugSupportInstallService.ts +++ b/src/papyrus-lang-vscode/src/debugger/DebugSupportInstallService.ts @@ -46,13 +46,13 @@ export class DebugSupportInstallService implements IDebugSupportInstallService { this._configProvider = configProvider; this._pathResolver = pathResolver; } - private _getMMPluginInstallPath(game: PapyrusGame, modsDir: string ): string { + private _getMMPluginInstallPath(game: PapyrusGame, modsDir: string): string { return path.join( modsDir, PDSModName, PathResolver._getModMgrExtenderPluginRelativePath(game), getPluginDllName(game, false) - ) + ); } // TODO: Refactor this properly, right now it's just hacked to work with MO2LaunchDescriptor async getInstallState(game: PapyrusGame, modsDir: string | undefined): Promise { diff --git a/src/papyrus-lang-vscode/src/debugger/GameDebugConfiguratorService.ts b/src/papyrus-lang-vscode/src/debugger/GameDebugConfiguratorService.ts index d67368a7..f594f0d0 100644 --- a/src/papyrus-lang-vscode/src/debugger/GameDebugConfiguratorService.ts +++ b/src/papyrus-lang-vscode/src/debugger/GameDebugConfiguratorService.ts @@ -1,7 +1,5 @@ // TODO: Remove, no longer necessary - - import { inject, injectable, interfaces } from 'inversify'; import { IExtensionConfigProvider } from '../ExtensionConfigProvider'; import { take } from 'rxjs/operators'; @@ -10,7 +8,7 @@ import { PapyrusGame, getGameIniName } from '../PapyrusGame'; import { ILanguageClientManager } from '../server/LanguageClientManager'; import { ClientHostStatus } from '../server/LanguageClientHost'; import { CheckIfDebuggingIsEnabledInIni, TurnOnDebuggingInIni } from '../common/GameHelpers'; -import { WriteChangesToIni, ParseIniFile } from "../common/INIHelpers"; +import { WriteChangesToIni, ParseIniFile } from '../common/INIHelpers'; import * as path from 'path'; import * as fs from 'fs'; @@ -26,7 +24,7 @@ export enum GameDebugConfigurationState { gameIniMissing, gameUserDirMissing, gameMissing, - gameDisabled + gameDisabled, } export interface IGameDebugConfiguratorService { @@ -60,7 +58,7 @@ export class GameDebugConfiguratorService implements IGameDebugConfiguratorServi return GameDebugConfigurationState.gameMissing; } } - const gameUserDirPath = gameUserDir || await this._pathResolver.getUserGamePath(game); + const gameUserDirPath = gameUserDir || (await this._pathResolver.getUserGamePath(game)); if (!gameUserDirPath) { return GameDebugConfigurationState.gameUserDirMissing; } @@ -78,11 +76,8 @@ export class GameDebugConfiguratorService implements IGameDebugConfiguratorServi return GameDebugConfigurationState.debugEnabled; } - async configureDebug( - game: PapyrusGame, - gameUserDir: string | undefined - ): Promise { - const gameUserDirPath = gameUserDir || await this._pathResolver.getUserGamePath(game); + async configureDebug(game: PapyrusGame, gameUserDir: string | undefined): Promise { + const gameUserDirPath = gameUserDir || (await this._pathResolver.getUserGamePath(game)); if (!gameUserDirPath) { return false; } diff --git a/src/papyrus-lang-vscode/src/debugger/MO2ConfiguratorService.ts b/src/papyrus-lang-vscode/src/debugger/MO2ConfiguratorService.ts index e0345c54..dc6caa15 100644 --- a/src/papyrus-lang-vscode/src/debugger/MO2ConfiguratorService.ts +++ b/src/papyrus-lang-vscode/src/debugger/MO2ConfiguratorService.ts @@ -2,22 +2,14 @@ import { inject, injectable, interfaces } from 'inversify'; import { IExtensionConfigProvider } from '../ExtensionConfigProvider'; import { take } from 'rxjs/operators'; import { IPathResolver } from '../common/PathResolver'; -import { PapyrusGame, getGameIniName } from '../PapyrusGame'; import { ILanguageClientManager } from '../server/LanguageClientManager'; -import { ClientHostStatus } from '../server/LanguageClientHost'; -import { getPIDsforFullPath, mkdirIfNeeded } from '../Utilities'; -import * as path from 'path'; import * as fs from 'fs'; import { promisify } from 'util'; -import md5File from 'md5-file'; -import { PDSModName } from '../common/constants'; import { IDebugSupportInstallService, DebugSupportInstallState } from './DebugSupportInstallService'; import { IAddressLibraryInstallService, AddressLibInstalledState } from './AddressLibInstallService'; import { MO2LauncherDescriptor } from './MO2LaunchDescriptorFactory'; -import { CheckIfDebuggingIsEnabledInIni, TurnOnDebuggingInIni } from '../common/GameHelpers'; -import { WriteChangesToIni, ParseIniFile } from '../common/INIHelpers'; import { AddRequiredModsToModList, checkAddressLibrariesExistAndEnabled, @@ -30,8 +22,6 @@ import * as MO2Lib from '../common/MO2Lib'; import { CancellationTokenSource } from 'vscode-languageclient'; import { CancellationToken } from 'vscode'; import { execFile as _execFile, spawn } from 'child_process'; -import { mkdir } from 'fs/promises'; -import { AddSeparatorToBeginningOfModList } from '../common/MO2Lib'; const execFile = promisify(_execFile); const exists = promisify(fs.exists); const copyFile = promisify(fs.copyFile); @@ -83,12 +73,12 @@ function _getErrorMessage(state: MO2LaunchConfigurationStatus) { } export function GetErrorMessageFromStatus(state: MO2LaunchConfigurationStatus): string { - let errorMessages = new Array(); - let states = getStates(state); + const errorMessages = new Array(); + const states = getStates(state); if (states.length === 1 && states[0] === MO2LaunchConfigurationStatus.Ready) { return 'Ready'; } - for (let state of states) { + for (const state of states) { errorMessages.push(_getErrorMessage(state)); } const errMsg = '- ' + errorMessages.join('\n - '); @@ -98,10 +88,10 @@ function getStates(state: MO2LaunchConfigurationStatus): MO2LaunchConfigurationS if (state === MO2LaunchConfigurationStatus.Ready) { return [MO2LaunchConfigurationStatus.Ready]; } - let states: MO2LaunchConfigurationStatus[] = []; + const states: MO2LaunchConfigurationStatus[] = []; let key: keyof typeof MO2LaunchConfigurationStatus; for (key in MO2LaunchConfigurationStatus) { - let value: MO2LaunchConfigurationStatus = Number(MO2LaunchConfigurationStatus[key]); + const value: MO2LaunchConfigurationStatus = Number(MO2LaunchConfigurationStatus[key]); if (state & value) { states.push(value); } @@ -153,7 +143,7 @@ export class MO2ConfiguratorService implements IMO2ConfiguratorService { } private async checkPDSisPresent(launchDescriptor: MO2LauncherDescriptor): Promise { - let result = await this._debugSupportInstallService.getInstallState( + const result = await this._debugSupportInstallService.getInstallState( launchDescriptor.game, launchDescriptor.instanceInfo.modsFolder ); @@ -175,7 +165,7 @@ export class MO2ConfiguratorService implements IMO2ConfiguratorService { private async checkAddressLibsArePresent( launchDescriptor: MO2LauncherDescriptor ): Promise { - let result = await this._addressLibraryInstallService.getInstallState( + const result = await this._addressLibraryInstallService.getInstallState( launchDescriptor.game, launchDescriptor.instanceInfo.modsFolder ); @@ -197,7 +187,7 @@ export class MO2ConfiguratorService implements IMO2ConfiguratorService { if (!modList) { return MO2LaunchConfigurationStatus.ModListNotParsable; } - let ret: MO2LaunchConfigurationStatus = MO2LaunchConfigurationStatus.Ready; + const ret: MO2LaunchConfigurationStatus = MO2LaunchConfigurationStatus.Ready; if (!checkPDSModExistsAndEnabled(modList)) { return MO2LaunchConfigurationStatus.PDSModNotEnabledInModList; } @@ -223,8 +213,8 @@ export class MO2ConfiguratorService implements IMO2ConfiguratorService { launchDescriptor: MO2LauncherDescriptor, cancellationToken = new CancellationTokenSource().token ): Promise { - let states = getStates(await this.getStateFromConfig(launchDescriptor)); - for (let state of states) { + const states = getStates(await this.getStateFromConfig(launchDescriptor)); + for (const state of states) { switch (state) { case MO2LaunchConfigurationStatus.Ready: break; @@ -260,8 +250,11 @@ export class MO2ConfiguratorService implements IMO2ConfiguratorService { if (await isMO2Running()) { wasRunning = true; // if ModOrganizer is currently running, and the installation or selected profile isn't what we're going to run, this will fuck up, kill it - let notOurs = !await isOurMO2Running(launchDescriptor.MO2EXEPath); - if (notOurs || launchDescriptor.instanceInfo.selectedProfile !== launchDescriptor.profileToLaunchData.name){ + const notOurs = !(await isOurMO2Running(launchDescriptor.MO2EXEPath)); + if ( + notOurs || + launchDescriptor.instanceInfo.selectedProfile !== launchDescriptor.profileToLaunchData.name + ) { await killAllMO2Processes(); } } @@ -278,10 +271,14 @@ export class MO2ConfiguratorService implements IMO2ConfiguratorService { return false; } if (wasRunning) { - spawn(launchDescriptor.MO2EXEPath, ['-p', launchDescriptor.profileToLaunchData.name, "refresh"], { - detached: true, - stdio: 'ignore', - }).unref(); + spawn( + launchDescriptor.MO2EXEPath, + ['-p', launchDescriptor.profileToLaunchData.name, 'refresh'], + { + detached: true, + stdio: 'ignore', + } + ).unref(); } break; default: diff --git a/src/papyrus-lang-vscode/src/debugger/MO2Helpers.ts b/src/papyrus-lang-vscode/src/debugger/MO2Helpers.ts index 4aabc931..43c53256 100644 --- a/src/papyrus-lang-vscode/src/debugger/MO2Helpers.ts +++ b/src/papyrus-lang-vscode/src/debugger/MO2Helpers.ts @@ -1,11 +1,6 @@ -import { exists, existsSync } from 'fs'; -import path, { normalize } from 'path'; -import { - getExecutableNameForGame, - getGameIniName, - getScriptExtenderExecutableName, - getScriptExtenderName, -} from '../PapyrusGame'; +import { existsSync } from 'fs'; +import path from 'path'; +import { getGameIniName } from '../PapyrusGame'; import { PapyrusGame } from '../PapyrusGame'; import { PDSModName } from '../common/constants'; import { DetermineGameVariant, FindUserGamePath, getAddressLibNames } from '../common/GameHelpers'; @@ -45,7 +40,7 @@ export function checkPDSModExistsAndEnabled(modlist: Array) export function checkAddressLibrariesExistAndEnabled(modlist: Array, game: PapyrusGame) { const names = getAddressLibNames(game); - for (let name of names) { + for (const name of names) { if (!MO2Lib.checkIfModExistsAndEnabled(modlist, name)) { return false; } @@ -63,12 +58,11 @@ export function AddRequiredModsToModList(p_modlist: Array, g modlist = MO2Lib.AddOrEnableModInModList(modlist, PDSModName); } if (addlibsNeeded) { - let addressLibraryMods = getAddressLibNames(game).map( + const addressLibraryMods = getAddressLibNames(game).map( (d) => new MO2Lib.ModListItem(d, MO2Lib.ModEnabledState.enabled) ); modlist = addressLibraryMods.reduce( - (_modlist, mod) => - MO2Lib.AddOrEnableModInModList(_modlist, mod.name), + (_modlist, mod) => MO2Lib.AddOrEnableModInModList(_modlist, mod.name), modlist ); } @@ -77,7 +71,7 @@ export function AddRequiredModsToModList(p_modlist: Array, g } export function GetMO2GameShortIdentifier(game: PapyrusGame): string { - let gamestring = + const gamestring = game === PapyrusGame.skyrimSpecialEdition ? 'skyrimse' : PapyrusGame[game].toLowerCase().replace(/ /g, ''); return gamestring; } @@ -98,26 +92,26 @@ export async function WasGameLaunchedWithMO2(game: PapyrusGame) { } const pid = pids[0]; // get env from process - let otherEnv = await getEnvFromProcess(pid); + const otherEnv = await getEnvFromProcess(pid); if (!otherEnv) { return false; } - let pathVar: string = otherEnv['Path']; + const pathVar: string = otherEnv['Path']; if (!pathVar) { return false; } - let pathVarSplit = pathVar.split(';'); + const pathVarSplit = pathVar.split(';'); if (pathVarSplit.length === 0 || !pathVarSplit[0]) { return false; } - let firstPath = path.normalize(pathVarSplit[0]); + const firstPath = path.normalize(pathVarSplit[0]); if (!firstPath) { return false; } - let basename = path.basename(firstPath); + const basename = path.basename(firstPath); if (basename.toLowerCase() === 'dlls') { - let parentdir = path.dirname(firstPath); - let MO2EXEPath = path.join(parentdir, MO2Lib.MO2EXEName); + const parentdir = path.dirname(firstPath); + const MO2EXEPath = path.join(parentdir, MO2Lib.MO2EXEName); if (existsSync(MO2EXEPath)) { return true; } @@ -129,8 +123,8 @@ export async function GetPossibleMO2InstancesForModFolder( modsFolder: string, game: PapyrusGame ): Promise { - let gameId = GetMO2GameID(game); - let instances = (await MO2Lib.FindAllKnownMO2EXEandInstanceLocations(gameId)).reduce((acc, val) => { + const gameId = GetMO2GameID(game); + const instances = (await MO2Lib.FindAllKnownMO2EXEandInstanceLocations(gameId)).reduce((acc, val) => { // Combine all the instances together, check to see if the mods folder is in the instance return acc.concat(val.instances.filter((d) => d.modsFolder === modsFolder)); }, [] as Array); @@ -138,7 +132,7 @@ export async function GetPossibleMO2InstancesForModFolder( return undefined; } //filter out the dupes from instances by comparing iniPaths - let filteredInstances = instances.filter((d, i) => { + const filteredInstances = instances.filter((d, i) => { return instances.findIndex((e) => e.iniPath === d.iniPath) === i; }); return filteredInstances; @@ -153,7 +147,7 @@ export async function getGameINIFromMO2Profile( // if [General] LocalSettings=false, then the game ini is in the global game save folder // if [General] LocalSettings=true, then the game ini is in the profile folder - const settingsFile = path.join(profileFolder, 'settings.ini') + const settingsFile = path.join(profileFolder, 'settings.ini'); const settingsIniData = await getMO2ProfileSettingsData(settingsFile); if (!settingsIniData) { throw new Error(`Could not get settings ini data`); @@ -208,13 +202,13 @@ export async function getMO2ProfileSettingsData(settingsIniPath: string): Promis export async function isMO2Running() { return (await getPIDforProcessName(MO2Lib.MO2EXEName)).length > 0; } -export async function isMO2ButNotThisOneRunning(MO2EXEPath: string){ +export async function isMO2ButNotThisOneRunning(MO2EXEPath: string) { const pids = await getPIDforProcessName(MO2Lib.MO2EXEName); if (pids.length === 0) { return false; } const ourPids = await getPIDsforFullPath(MO2EXEPath); - if (ourPids.length === 0 ) { + if (ourPids.length === 0) { return true; } return pids.some((pid) => ourPids.indexOf(pid) === -1); diff --git a/src/papyrus-lang-vscode/src/debugger/MO2LaunchDescriptorFactory.ts b/src/papyrus-lang-vscode/src/debugger/MO2LaunchDescriptorFactory.ts index 5135b955..f88b9b40 100644 --- a/src/papyrus-lang-vscode/src/debugger/MO2LaunchDescriptorFactory.ts +++ b/src/papyrus-lang-vscode/src/debugger/MO2LaunchDescriptorFactory.ts @@ -87,7 +87,7 @@ export class MO2LaunchDescriptorFactory implements IMO2LaunchDescriptorFactory { if (!InstanceInfo) { throw new Error(`Could not find the instance '${instanceName}' for the MO2 installation at ${MO2EXEPath}`); } - let papgame = GetPapyrusGameFromMO2GameID(InstanceInfo.gameName); + const papgame = GetPapyrusGameFromMO2GameID(InstanceInfo.gameName); if (!papgame || papgame !== game) { throw new Error(`Instance ${instanceName} is not for game ${game}`); } @@ -113,12 +113,12 @@ export class MO2LaunchDescriptorFactory implements IMO2LaunchDescriptorFactory { game: PapyrusGame ): Promise { // taken care of by debug config provider - let { instanceName, exeName } = MO2Lib.parseMoshortcutURI(mo2Config.shortcutURI); + const { instanceName, exeName } = MO2Lib.parseMoshortcutURI(mo2Config.shortcutURI); if (!instanceName || !exeName) { throw new Error(`Could not parse the shortcut URI`); } - let MO2EXEPath = launcherPath; + const MO2EXEPath = launcherPath; if (!MO2EXEPath || !existsSync(MO2EXEPath) || !statSync(MO2EXEPath).isFile()) { throw new Error(`Could not find the Mod Organizer 2 executable path`); } @@ -193,8 +193,8 @@ export interface IMO2LauncherDescriptor { } function joinArgs(args: string[]): string { - let _args = args; - for (let arg in args) { + const _args = args; + for (const arg in args) { if (_args[arg].includes(' ') && !_args[arg].startsWith('"') && !_args[arg].endsWith('"')) { _args[arg] = `"${_args[arg]}"`; } @@ -219,7 +219,7 @@ export class MO2LauncherDescriptor implements IMO2LauncherDescriptor { } public getLaunchCommand(): LaunchCommand { - let command = this.MO2EXEPath; + const command = this.MO2EXEPath; let cmdargs = ['-p', this.profileToLaunchData.name]; if (this.instanceInfo.name !== 'portable') { cmdargs = cmdargs.concat(['-i', this.instanceInfo.name]); diff --git a/src/papyrus-lang-vscode/src/debugger/PapyrusDebugAdapterDescriptorFactory.ts b/src/papyrus-lang-vscode/src/debugger/PapyrusDebugAdapterDescriptorFactory.ts index 9d5854d1..ff23e230 100644 --- a/src/papyrus-lang-vscode/src/debugger/PapyrusDebugAdapterDescriptorFactory.ts +++ b/src/papyrus-lang-vscode/src/debugger/PapyrusDebugAdapterDescriptorFactory.ts @@ -15,8 +15,8 @@ import { getDisplayNameForGame, getScriptExtenderName, getScriptExtenderUrl, - getShortDisplayNameForGame -} from "../PapyrusGame"; + getShortDisplayNameForGame, +} from '../PapyrusGame'; import { ICreationKitInfoProvider } from '../CreationKitInfoProvider'; import { IExtensionConfigProvider } from '../ExtensionConfigProvider'; import { take } from 'rxjs/operators'; @@ -28,12 +28,16 @@ import { ILanguageClientManager } from '../server/LanguageClientManager'; import { showGameDisabledMessage, showGameMissingMessage } from '../features/commands/InstallDebugSupportCommand'; import { inject, injectable } from 'inversify'; import { DebugLaunchState, IDebugLauncherService, LaunchCommand } from './DebugLauncherService'; -import { IMO2LauncherDescriptor, IMO2LaunchDescriptorFactory, MO2LaunchDescriptorFactory } from './MO2LaunchDescriptorFactory'; -import { GetErrorMessageFromStatus, IMO2ConfiguratorService, MO2ConfiguratorService, MO2LaunchConfigurationStatus } from './MO2ConfiguratorService'; +import { IMO2LauncherDescriptor, IMO2LaunchDescriptorFactory } from './MO2LaunchDescriptorFactory'; +import { + GetErrorMessageFromStatus, + IMO2ConfiguratorService, + MO2LaunchConfigurationStatus, +} from './MO2ConfiguratorService'; import path from 'path'; import * as fs from 'fs'; import { promisify } from 'util'; -import { isMO2ButNotThisOneRunning, isMO2Running, isOurMO2Running, killAllMO2Processes } from './MO2Helpers'; +import { isMO2ButNotThisOneRunning, killAllMO2Processes } from './MO2Helpers'; const exists = promisify(fs.exists); const noopExecutable = new DebugAdapterExecutable('node', ['-e', '""']); @@ -52,8 +56,6 @@ function getDefaultPortForGame(game: PapyrusGame) { return game === PapyrusGame.fallout4 ? 2077 : 43201; } - - @injectable() export class PapyrusDebugAdapterDescriptorFactory implements DebugAdapterDescriptorFactory { private readonly _languageClientManager: ILanguageClientManager; @@ -112,18 +114,22 @@ export class PapyrusDebugAdapterDescriptorFactory implements DebugAdapterDescrip env.openExternal(Uri.parse(getScriptExtenderUrl(game))); break; } - return false + return false; } - private async _ShowLaunchDebugSupportInstallMessage(game: PapyrusGame, launchType: 'MO2' | 'XSE', launcher: IMO2LauncherDescriptor) { + private async _ShowLaunchDebugSupportInstallMessage( + game: PapyrusGame, + launchType: 'MO2' | 'XSE', + launcher: IMO2LauncherDescriptor + ) { const installOption = `Fix Configuration`; const state = await this._MO2ConfiguratorService.getStateFromConfig(launcher); - if (state !== MO2LaunchConfigurationStatus.Ready){ + if (state !== MO2LaunchConfigurationStatus.Ready) { const errorMessage = GetErrorMessageFromStatus(state); const selectedInstallOption = await window.showInformationMessage( - `The following configuration problems were encountered while attempting to launch ${getDisplayNameForGame(game)}:\n${ - errorMessage - }\nWould you like to fix the configuration?`, + `The following configuration problems were encountered while attempting to launch ${getDisplayNameForGame( + game + )}:\n${errorMessage}\nWould you like to fix the configuration?`, installOption, 'Cancel' ); @@ -137,7 +143,7 @@ export class PapyrusDebugAdapterDescriptorFactory implements DebugAdapterDescrip break; case 'Cancel': return true; - } + } } return false; } @@ -171,7 +177,7 @@ export class PapyrusDebugAdapterDescriptorFactory implements DebugAdapterDescrip } break; } - + case DebugSupportInstallState.notInstalled: return await this._ShowAttachDebugSupportInstallMessage(game); case DebugSupportInstallState.gameDisabled: @@ -211,31 +217,40 @@ export class PapyrusDebugAdapterDescriptorFactory implements DebugAdapterDescrip } let launched = DebugLaunchState.success; - if (session.configuration.request === 'launch'){ + if (session.configuration.request === 'launch') { // check if the game is running - if (await getGameIsRunning(game)){ - throw new Error(`'${getDisplayNameForGame(game)}' is already running. Please close it before launching the debugger.`); + if (await getGameIsRunning(game)) { + throw new Error( + `'${getDisplayNameForGame( + game + )}' is already running. Please close it before launching the debugger.` + ); } // run the launcher with the args from the configuration // if the launcher is MO2 - let launcherPath: string = session.configuration.launcherPath || ""; - if (!launcherPath){ + let launcherPath: string = session.configuration.launcherPath || ''; + if (!launcherPath) { throw new Error(`'Invalid launch configuration. Launcher path is missing.`); } launcherPath = path.normalize(launcherPath); - if (!launcherPath || !await exists(launcherPath)){ - throw new Error(`'Path does not exist!`) + if (!launcherPath || !(await exists(launcherPath))) { + throw new Error(`'Path does not exist!`); } - let launcherArgs: string[] = session.configuration.args || []; + const launcherArgs: string[] = session.configuration.args || []; let LauncherCommand: LaunchCommand; - if(session.configuration.launchType === 'MO2') { - if (session.configuration.mo2Config === undefined){ + if (session.configuration.launchType === 'MO2') { + if (session.configuration.mo2Config === undefined) { throw new Error(`'Invalid launch configuration. MO2 configuration is missing.`); } - let launcher = await this._MO2LaunchDescriptorFactory.createMO2LaunchDecriptor(launcherPath, launcherArgs, session.configuration.mo2Config, game); - let state = await this._MO2ConfiguratorService.getStateFromConfig(launcher); + const launcher = await this._MO2LaunchDescriptorFactory.createMO2LaunchDecriptor( + launcherPath, + launcherArgs, + session.configuration.mo2Config, + game + ); + const state = await this._MO2ConfiguratorService.getStateFromConfig(launcher); if (state !== MO2LaunchConfigurationStatus.Ready) { - if (!await this._ShowLaunchDebugSupportInstallMessage(game, 'MO2', launcher)){ + if (!(await this._ShowLaunchDebugSupportInstallMessage(game, 'MO2', launcher))) { session.configuration.noop = true; return noopExecutable; } @@ -245,12 +260,14 @@ export class PapyrusDebugAdapterDescriptorFactory implements DebugAdapterDescrip LauncherCommand = launcher.getLaunchCommand(); // If MO2 is running and the profile is not the one we want to launch, the launch will fuck up, kill it - if (await isMO2ButNotThisOneRunning(launcher.MO2EXEPath) || (launcher.instanceInfo.selectedProfile !== launcher.profileToLaunchData.name)){ + if ( + (await isMO2ButNotThisOneRunning(launcher.MO2EXEPath)) || + launcher.instanceInfo.selectedProfile !== launcher.profileToLaunchData.name + ) { await killAllMO2Processes(); } - - } else if(session.configuration.launchType === 'XSE') { - LauncherCommand = {command: launcherPath, args: launcherArgs}; + } else if (session.configuration.launchType === 'XSE') { + LauncherCommand = { command: launcherPath, args: launcherArgs }; } else { // throw an error indicated the launch configuration is invalid throw new Error(`'Invalid launch configuration.`); @@ -259,29 +276,34 @@ export class PapyrusDebugAdapterDescriptorFactory implements DebugAdapterDescrip const cancellationSource = new CancellationTokenSource(); const cancellationToken = cancellationSource.token; const port = session.configuration.port || getDefaultPortForGame(game); - let wait_message = window.setStatusBarMessage(`Waiting for ${getDisplayNameForGame(game)} to start...`, 30000); - launched = await this._debugLauncher.runLauncher( LauncherCommand, game, port, cancellationToken) + const wait_message = window.setStatusBarMessage( + `Waiting for ${getDisplayNameForGame(game)} to start...`, + 30000 + ); + launched = await this._debugLauncher.runLauncher(LauncherCommand, game, port, cancellationToken); wait_message.dispose(); } else { - if (!await this._attachEnsureGameInstalled(game)){ + if (!(await this._attachEnsureGameInstalled(game))) { session.configuration.noop = true; return noopExecutable; - } + } } - if (launched != DebugLaunchState.success){ - if (launched === DebugLaunchState.cancelled){ + if (launched != DebugLaunchState.success) { + if (launched === DebugLaunchState.cancelled) { session.configuration.noop = true; - return noopExecutable; + return noopExecutable; } - if (launched === DebugLaunchState.multipleGamesRunning){ - const errMessage = `Multiple ${getDisplayNameForGame(game)} instances are running, shut them down and try again.`; + if (launched === DebugLaunchState.multipleGamesRunning) { + const errMessage = `Multiple ${getDisplayNameForGame( + game + )} instances are running, shut them down and try again.`; window.showErrorMessage(errMessage); } // throw an error indicating the launch failed throw new Error(`'${game}' failed to launch.`); - // attach - } else if (!await this.ensureGameRunning(game)) { + // attach + } else if (!(await this.ensureGameRunning(game))) { session.configuration.noop = true; return noopExecutable; } diff --git a/src/papyrus-lang-vscode/src/debugger/PapyrusDebugAdapterTracker.ts b/src/papyrus-lang-vscode/src/debugger/PapyrusDebugAdapterTracker.ts index d1aa75c7..4419c260 100644 --- a/src/papyrus-lang-vscode/src/debugger/PapyrusDebugAdapterTracker.ts +++ b/src/papyrus-lang-vscode/src/debugger/PapyrusDebugAdapterTracker.ts @@ -7,9 +7,7 @@ export class PapyrusDebugAdapterTrackerFactory implements DebugAdapterTrackerFac private readonly _debugLauncher: IDebugLauncherService; private readonly _registration: Disposable; - constructor( - @inject(IDebugLauncherService) debugLauncher: IDebugLauncherService - ) { + constructor(@inject(IDebugLauncherService) debugLauncher: IDebugLauncherService) { this._debugLauncher = debugLauncher; this._registration = debug.registerDebugAdapterTrackerFactory('papyrus', this); } @@ -28,9 +26,7 @@ export class PapyrusDebugAdapterTracker implements DebugAdapterTracker { private _showErrorMessages = true; - constructor(session: DebugSession, - debugLauncher: IDebugLauncherService - ) { + constructor(session: DebugSession, debugLauncher: IDebugLauncherService) { this._debugLauncher = debugLauncher; this._session = session; } diff --git a/src/papyrus-lang-vscode/src/debugger/PapyrusDebugConfigurationProvider.ts b/src/papyrus-lang-vscode/src/debugger/PapyrusDebugConfigurationProvider.ts index a003d89d..7bbe066e 100644 --- a/src/papyrus-lang-vscode/src/debugger/PapyrusDebugConfigurationProvider.ts +++ b/src/papyrus-lang-vscode/src/debugger/PapyrusDebugConfigurationProvider.ts @@ -1,13 +1,5 @@ import { inject, injectable } from 'inversify'; -import { - ProviderResult, - DebugConfigurationProvider, - CancellationToken, - WorkspaceFolder, - debug, - Disposable, - DebugConfiguration, -} from 'vscode'; +import { DebugConfigurationProvider, CancellationToken, WorkspaceFolder, debug, Disposable } from 'vscode'; import { IPathResolver } from '../common/PathResolver'; import { PapyrusGame } from '../PapyrusGame'; import { GetPapyrusGameFromMO2GameID } from './MO2Helpers'; @@ -37,14 +29,14 @@ export class PapyrusDebugConfigurationProvider implements DebugConfigurationProv token?: CancellationToken // TODO: FIX THIS ): Promise { - let PapyrusAttach = { + const PapyrusAttach = { type: 'papyrus', name: 'Fallout 4', game: PapyrusGame.fallout4, request: 'attach', projectPath: '${workspaceFolder}/fallout4.ppj', } as IPapyrusDebugConfiguration; - let PapyrusMO2Launch = { + const PapyrusMO2Launch = { type: 'papyrus', name: 'Fallout 4 (Launch with MO2)', game: PapyrusGame.fallout4, @@ -52,10 +44,10 @@ export class PapyrusDebugConfigurationProvider implements DebugConfigurationProv launchType: 'MO2', launcherPath: 'C:/Modding/MO2/ModOrganizer.exe', mo2Config: { - shortcutURI: 'moshortcut://Fallout 4:F4SE' + shortcutURI: 'moshortcut://Fallout 4:F4SE', } as MO2Config, } as IPapyrusDebugConfiguration; - let PapyruseXSELaunch = { + const PapyruseXSELaunch = { type: 'papyrus', name: 'Fallout 4 (Launch with F4SE)', game: PapyrusGame.fallout4, @@ -98,17 +90,17 @@ export class PapyrusDebugConfigurationProvider implements DebugConfigurationProv // substitute all the environment variables in the given string // environment variables are of the form ${env:VARIABLE_NAME} async substituteEnvVars(string: string): Promise { - let envVars = string.match(/\$\{env:([^\}]+)\}/g); + const envVars = string.match(/\$\{env:([^\}]+)\}/g); if (envVars !== null) { - for (let envVar of envVars) { + for (const envVar of envVars) { if (envVar === undefined || envVar === null) { continue; } - let matches = envVar?.match(/\$\{env:([^\}]+)\}/); + const matches = envVar?.match(/\$\{env:([^\}]+)\}/); if (matches === null || matches.length < 2) { continue; } - let envVarName = matches[1]; + const envVarName = matches[1]; let envVarValue: string | undefined; switch (envVarName) { @@ -128,7 +120,7 @@ export class PapyrusDebugConfigurationProvider implements DebugConfigurationProv envVarValue = undefined; break; } - + if (envVarValue === undefined) { envVarValue = ''; } @@ -142,8 +134,8 @@ export class PapyrusDebugConfigurationProvider implements DebugConfigurationProv async prepMo2Config(launcherPath: string, mo2Config: MO2Config, game: PapyrusGame): Promise { let instanceINI = mo2Config.instanceIniPath; if (!instanceINI) { - let { instanceName } = parseMoshortcutURI(mo2Config.shortcutURI); - let instanceInfo = await FindInstanceForEXE(launcherPath, instanceName); + const { instanceName } = parseMoshortcutURI(mo2Config.shortcutURI); + const instanceInfo = await FindInstanceForEXE(launcherPath, instanceName); if ( instanceInfo && GetPapyrusGameFromMO2GameID(instanceInfo.gameName) && @@ -152,12 +144,14 @@ export class PapyrusDebugConfigurationProvider implements DebugConfigurationProv instanceINI = instanceInfo.iniPath; } } else { - instanceINI = mo2Config.instanceIniPath ? await this.substituteEnvVars(mo2Config.instanceIniPath) : mo2Config.instanceIniPath; + instanceINI = mo2Config.instanceIniPath + ? await this.substituteEnvVars(mo2Config.instanceIniPath) + : mo2Config.instanceIniPath; } return { shortcutURI: mo2Config.shortcutURI, profile: mo2Config.profile, - instanceIniPath: instanceINI + instanceIniPath: instanceINI, } as MO2Config; } @@ -167,7 +161,7 @@ export class PapyrusDebugConfigurationProvider implements DebugConfigurationProv token?: CancellationToken ): Promise { if (debugConfiguration.request === 'launch' && debugConfiguration.launcherPath) { - let path = await this.substituteEnvVars(debugConfiguration.launcherPath); + const path = await this.substituteEnvVars(debugConfiguration.launcherPath); if (path === undefined) { throw new Error('Invalid debug configuration.'); } diff --git a/src/papyrus-lang-vscode/src/debugger/PapyrusDebugSession.ts b/src/papyrus-lang-vscode/src/debugger/PapyrusDebugSession.ts index 3b651f70..9a018e36 100644 --- a/src/papyrus-lang-vscode/src/debugger/PapyrusDebugSession.ts +++ b/src/papyrus-lang-vscode/src/debugger/PapyrusDebugSession.ts @@ -1,5 +1,5 @@ import { DebugSession, DebugConfiguration } from 'vscode'; -import { PapyrusGame } from "../PapyrusGame"; +import { PapyrusGame } from '../PapyrusGame'; export interface IPapyrusDebugSession extends DebugSession { readonly type: 'papyrus'; @@ -9,33 +9,33 @@ export interface IPapyrusDebugSession extends DebugSession { export interface MO2Config { /** * The shortcut URI for the Mod Organizer 2 profile to launch - * + * * You can get this from the Mod Organizer 2 shortcut menu. - * + * * It is in the format: `moshortcut://:`. - * + * * If the MO2 installation is portable, the instance name is blank. - * - * - * + * + * + * * Examples: * - non-portable: `moshortcut://Skyrim Special Edition:SKSE` * - portable: `moshortcut://:F4SE` - */ + */ shortcutURI: string; /** * The name of the Mod Organizer 2 profile to launch with. - * + * * Defaults to the currently selected profile */ profile?: string; /** * The path to the Mod Organizer 2 instance ini for this game. * This is only necessary to be set if the debugger has difficulty finding the MO2 instance location - * + * * - If the Mod Organizer 2 exe is a portable installation, this is located in the parent folder. * - If it is a non-portable installation, this in `%LOCALAPPDATA%/ModOrganizer//ModOrganizer.ini` - * + * * Examples: * - `C:/Users//AppData/Local/ModOrganizer/Fallout4/ModOrganizer.ini` * - `C:/Modding/MO2/ModOrganizer.ini` @@ -43,13 +43,12 @@ export interface MO2Config { instanceIniPath?: string; } -export interface XSEConfig { -} +export interface XSEConfig {} export interface IPapyrusDebugConfiguration extends DebugConfiguration { - /** + /** * The game to debug ('fallout4', 'skyrim', 'skyrimSpecialEdition') - */ + */ game: PapyrusGame; /** * The path to the project to debug @@ -74,10 +73,10 @@ export interface IPapyrusDebugConfiguration extends DebugConfiguration { /** * The path to the launcher executable - * + * * - If the launch type is 'MO2', this is the path to the Mod Organizer 2 executable. * - If the launch type is 'XSE', this is the path to the f4se/skse loader executable. - * + * * Examples: * - "C:/Program Files/Mod Organizer 2/ModOrganizer.exe" * - "C:/Program Files (x86)/Steam/steamapps/common/Skyrim Special Edition/skse64_loader.exe" @@ -87,15 +86,14 @@ export interface IPapyrusDebugConfiguration extends DebugConfiguration { launcherPath?: string; /** - * + * * Configuration for Mod Organizer 2 - * + * * Only used if launchType is 'MO2' - * + * */ mo2Config?: MO2Config; - /** * (optional, advanced) Additional arguments to pass to the launcher */ diff --git a/src/papyrus-lang-vscode/src/debugger/PexParser.ts b/src/papyrus-lang-vscode/src/debugger/PexParser.ts index 5a4cd3ac..4bb3bd95 100644 --- a/src/papyrus-lang-vscode/src/debugger/PexParser.ts +++ b/src/papyrus-lang-vscode/src/debugger/PexParser.ts @@ -46,88 +46,87 @@ export interface PexHeader { } export interface PexBinary { - Path: string; - Header: PexHeader; - DebugInfo: DebugInfo; - // TODO: add the rest of the data + Path: string; + Header: PexHeader; + DebugInfo: DebugInfo; + // TODO: add the rest of the data } -export enum FunctionType{ - Method, - Getter, - Setter +export enum FunctionType { + Method, + Getter, + Setter, } // names these are all indexed into the string table, but copied into the interface export interface FunctionInfo { - ObjectName: string; - StateName: string; - FunctionName: string; - FunctionType: FunctionType; - LineNumbers: number[]; + ObjectName: string; + StateName: string; + FunctionName: string; + FunctionType: FunctionType; + LineNumbers: number[]; } export interface PropertyGroup { - ObjectName: string, - GroupName: string, - DocString: string, - UserFlags: number, - Names: string[] + ObjectName: string; + GroupName: string; + DocString: string; + UserFlags: number; + Names: string[]; } export interface StructOrder { - StructName: string, - OrderName: string, - Names: string[] + StructName: string; + OrderName: string; + Names: string[]; } export interface DebugInfo { - ModificationTime: number, - FunctionInfos: FunctionInfo[], - PropertyGroups: PropertyGroup[], - /** - * Fallout 4 only - */ - StructOrders?: StructOrder[] + ModificationTime: number; + FunctionInfos: FunctionInfo[]; + PropertyGroups: PropertyGroup[]; + /** + * Fallout 4 only + */ + StructOrders?: StructOrder[]; } // TODO: maybe implement this class PexIndexedString { - public readonly index: number; - public readonly str: string; - constructor(index: number, str: string) { - this.index = index; - this.str = str; - } - - public toString(): string { - return this.str; - } + public readonly index: number; + public readonly str: string; + constructor(index: number, str: string) { + this.index = index; + this.str = str; + } + + public toString(): string { + return this.str; + } } export class PexStringTable { - public readonly strings: string[]; - constructor(strings: string[]) { - this.strings = strings; - } + public readonly strings: string[]; + constructor(strings: string[]) { + this.strings = strings; + } } const LE_MAGIC_NUMBER = 0xfa57c0de; // 4200055006, values when read little endian const BE_MAGIC_NUMBER = 0xdec057fa; // 3737147386, values when read little endian - function DetermineEndianness(buffer: Buffer) { - const magicNumber = buffer.readUInt32LE(0); - return DetermineEndiannessFromNumber(magicNumber); + const magicNumber = buffer.readUInt32LE(0); + return DetermineEndiannessFromNumber(magicNumber); } function DetermineEndiannessFromNumber(number: number) { - if (number === LE_MAGIC_NUMBER) { - return 'little'; - } else if (number === BE_MAGIC_NUMBER) { - return 'big'; - } else { - return undefined; - } + if (number === LE_MAGIC_NUMBER) { + return 'little'; + } else if (number === BE_MAGIC_NUMBER) { + return 'big'; + } else { + return undefined; + } } function getGameFromEndianness(endianness: 'little' | 'big') { @@ -139,268 +138,272 @@ function getGameFromEndianness(endianness: 'little' | 'big') { /** * Parses a pex file. - * + * * NOTE: This only currently implements the parsing of the header and the debug info. */ export class PexReader { public readonly path; - + public readonly game: PapyrusGame = PapyrusGame.skyrimSpecialEdition; constructor(path: string) { this.path = path; } - private endianness: "little" | "big" = "little"; + private endianness: 'little' | 'big' = 'little'; private stringTable: PexStringTable = new PexStringTable([]); - // constants - - // parsers - private readonly StringParser = () => - new Parser() - .uint16('__strlen') - .string('__string', { length: '__strlen', encoding: 'ascii', zeroTerminated: false }); - - private readonly _strNest = { type: this.StringParser(), formatter: (x:any): string => x.__string }; - - private readonly StringTableParser = - new Parser().uint16('__tbllen').array('__strings', { - type: this.StringParser(), - formatter: (x:any): string[] => x.map((y:any) => y.__string), - length: '__tbllen', - }); - - private readonly _strTableNest = { type: this.StringTableParser , formatter: (x:any): - PexStringTable => { - // TODO: Global state hack to get around not being able to reference the parsed string table in the middle of the parse - this.stringTable = new PexStringTable(x.__strings); - return this.stringTable; + // constants + + // parsers + private readonly StringParser = () => + new Parser() + .uint16('__strlen') + .string('__string', { length: '__strlen', encoding: 'ascii', zeroTerminated: false }); + + private readonly _strNest = { type: this.StringParser(), formatter: (x: any): string => x.__string }; + + private readonly StringTableParser = new Parser().uint16('__tbllen').array('__strings', { + type: this.StringParser(), + formatter: (x: any): string[] => x.map((y: any) => y.__string), + length: '__tbllen', + }); + + private readonly _strTableNest = { + type: this.StringTableParser, + formatter: (x: any): PexStringTable => { + // TODO: Global state hack to get around not being able to reference the parsed string table in the middle of the parse + this.stringTable = new PexStringTable(x.__strings); + return this.stringTable; + }, + }; + + private readonly FunctionInfoRawParser = () => + new Parser() + .uint16('ObjectName') + .uint16('StateName') + .uint16('FunctionName') + .uint8('FunctionType') + .uint16('LineNumbersCount') + .array('LineNumbers', { + type: this.GetUintType(), + length: 'LineNumbersCount', + formatter: (x: any): number[] => x.map((y: any) => y.__val), + }); + + private readonly FunctionInfosParser = () => + new Parser().uint16('__FIlen').array('__infos', { + type: this.FunctionInfoRawParser(), + length: '__FIlen', + formatter: (x: any): FunctionInfo[] => + x.map((y: any) => { + const functinfo = { + ObjectName: this.TableLookup(y.ObjectName), + StateName: this.TableLookup(y.StateName), + FunctionName: this.TableLookup(y.FunctionName), + FunctionType: y.FunctionType as FunctionType, + LineNumbers: y.LineNumbers, + } as FunctionInfo; + return functinfo; + }), + }); + public GetEndianness() { + return this.endianness; } - }; - - private readonly FunctionInfoRawParser = () => new Parser() - .uint16('ObjectName') - .uint16('StateName') - .uint16('FunctionName') - .uint8('FunctionType') - .uint16('LineNumbersCount') - .array('LineNumbers', { - type: this.GetUintType(), - length: 'LineNumbersCount', - formatter: (x:any): number[] => x.map((y:any) => y.__val) - }) - - - private readonly FunctionInfosParser = () => new Parser().uint16('__FIlen').array('__infos', { - type: this.FunctionInfoRawParser(), - length: '__FIlen', - formatter: (x:any): FunctionInfo[] => x.map((y:any) => { - let functinfo = { - ObjectName: this.TableLookup(y.ObjectName), - StateName: this.TableLookup(y.StateName), - FunctionName: this.TableLookup(y.FunctionName), - FunctionType: y.FunctionType as FunctionType, - LineNumbers: y.LineNumbers - } as FunctionInfo; - return functinfo; - }), - }); - public GetEndianness() { - return this.endianness; - } - private GetUintType(){ - return new Parser().uint16("__val"); - } - private readonly PropertyGroupRawParser = () => new Parser() - .uint16('ObjectName') - .uint16('GroupName') - .uint16('DocString') - .uint32('UserFlags') - .uint16('NamesCount') - .array('Names', { - type: this.GetUintType(), - length: 'NamesCount', - formatter: (x:any): number[] => x.map((y:any) => y.__val) - }) - private TableLookup (x: number){ - if (x >= this.stringTable.strings.length){ - return ""; + private GetUintType() { + return new Parser().uint16('__val'); } - return this.stringTable.strings[x]; - } - private readonly PropertyGroupsParser = () => new Parser().uint16('__PGlen').array('__infos', { - type: this.PropertyGroupRawParser(), - length: '__PGlen', - formatter: (x:any): PropertyGroup[] => x.map((y:any) => { - let pgroups = { - ObjectName: this.TableLookup(y.ObjectName), - GroupName: this.TableLookup(y.GroupName), - DocString: this.TableLookup(y.DocString), - UserFlags: y.UserFlags, - Names: y.Names.map((z:any) => this.TableLookup(z)) - } as PropertyGroup; - return pgroups; - }) - }); - - private readonly StructOrderRawParser = () => new Parser() - .uint16('StructName') - .uint16('OrderName') - .uint16('NamesCount') - .array('Names', { - type: this.GetUintType(), - length: 'NamesCount', - formatter: (x:any): number[] => x.map((y:any) => y.__val) - - }) - - private readonly StructOrdersParser = () => new Parser().uint16('__SOlen').array('__infos', { - type: this.StructOrderRawParser(), - length: '__SOlen', - formatter: (x:any): StructOrder[] => x.map((y:any) => { - let sorders = { - StructName: this.TableLookup(y.StructName), - OrderName: this.TableLookup(y.OrderName), - Names: y.Names.map((z:any) => this.TableLookup(z)) - } as StructOrder; - return sorders; - }) - }); - - private readonly _doParseDebugInfo = () => { - return new Parser() - .uint64('ModificationTime') - .nest('FunctionInfos', { - type: this.FunctionInfosParser(), - formatter: (x:any): FunctionInfo[] => x.__infos - }) - .nest('PropertyGroups', { - type: this.PropertyGroupsParser(), - formatter: (x:any): PropertyGroup[] => x.__infos - }) - .choice("StructOrders", { - tag: () => { - let val =this.endianness === "little" ? 1 : 0 - return val; - }, - choices: { - 0: new Parser().skip(0), - 1: this.StructOrdersParser() - }, - formatter: (x:any): StructOrder[] | undefined => { - if (this.endianness === "little" && x){ - return x.__infos; - } - return undefined; + private readonly PropertyGroupRawParser = () => + new Parser() + .uint16('ObjectName') + .uint16('GroupName') + .uint16('DocString') + .uint32('UserFlags') + .uint16('NamesCount') + .array('Names', { + type: this.GetUintType(), + length: 'NamesCount', + formatter: (x: any): number[] => x.map((y: any) => y.__val), + }); + private TableLookup(x: number) { + if (x >= this.stringTable.strings.length) { + return ''; } - }) + return this.stringTable.strings[x]; } - - - private readonly ParseDebugInfo = () => new Parser() - .uint8('HasDebugInfo') - .choice('DebugInfo', { - tag: 'HasDebugInfo', - choices: { - 0: new Parser().skip(0), - 1: this._doParseDebugInfo() - }, - formatter: (x:any): DebugInfo | undefined => { - if (!x) { - return undefined; - } - return x; - } - }) - - private readonly _debugInfoNest = { type: this.ParseDebugInfo(), formatter: (x:any): DebugInfo | undefined => x ? x.DebugInfo : undefined }; - - private readonly HeaderParser = () => - new Parser() - .uint32('MagicNumber') - .uint8('MajorVersion') - .uint8('MinorVersion') - .uint16('GameID') - .uint64('CompileTime') - .nest('SourceFileName', this._strNest) - .nest('UserName', this._strNest) - .nest('ComputerName',this. _strNest); - - private readonly _HeaderNest = (endianness: 'little' | 'big') => { - return { - type: this.HeaderParser(), - formatter: (x: any): PexHeader => { - return { - Game: getGameFromEndianness(endianness), - MajorVersion: x.MajorVersion, - MinorVersion: x.MinorVersion, - GameID: x.GameID, - CompileTime: x.CompileTime, - SourceFileName: x.SourceFileName, - UserName: x.UserName, - ComputerName: x.ComputerName, - }; - }, - }; - }; - - - private ReadPexBinary(buffer: Buffer): PexBinary | undefined { - let endianness: 'little' | 'big' | undefined = DetermineEndianness(buffer); - if (!endianness) { - return undefined; - } - const Pex = new Parser() - .endianess(endianness) - .nest('Header', this._HeaderNest(endianness)) - .nest('StringTable',this._strTableNest) - .nest('DebugInfo', this._debugInfoNest) - .parse(buffer); - - return { - Path: this.path, - Header: Pex.Header, - DebugInfo: Pex.DebugInfo - } - } - - private ReadHeader(buffer: Buffer) { - - return new Parser() - .endianess(this.endianness) - .nest('Header', this._HeaderNest(this.endianness)).parse(buffer); - } - - public async ReadPexHeader(): Promise { - // read the binary file from the path into a byte buffer - if (!fs.existsSync(this.path) || !fs.lstatSync(this.path).isFile()) { - return undefined; - } - - const buffer = fs.readFileSync(this.path); - if (!buffer || buffer.length < 4) { - return undefined; - } - let endianness: 'little' | 'big' | undefined = DetermineEndianness(buffer); - if (!endianness) { - return undefined; - } - this.endianness = endianness; - - return this.ReadHeader(buffer); - } - // not complete - async ReadPex(): Promise{ - const buffer = fs.readFileSync(this.path); - if (!buffer || buffer.length < 4) { - return undefined; + private readonly PropertyGroupsParser = () => + new Parser().uint16('__PGlen').array('__infos', { + type: this.PropertyGroupRawParser(), + length: '__PGlen', + formatter: (x: any): PropertyGroup[] => + x.map((y: any) => { + const pgroups = { + ObjectName: this.TableLookup(y.ObjectName), + GroupName: this.TableLookup(y.GroupName), + DocString: this.TableLookup(y.DocString), + UserFlags: y.UserFlags, + Names: y.Names.map((z: any) => this.TableLookup(z)), + } as PropertyGroup; + return pgroups; + }), + }); + + private readonly StructOrderRawParser = () => + new Parser() + .uint16('StructName') + .uint16('OrderName') + .uint16('NamesCount') + .array('Names', { + type: this.GetUintType(), + length: 'NamesCount', + formatter: (x: any): number[] => x.map((y: any) => y.__val), + }); + + private readonly StructOrdersParser = () => + new Parser().uint16('__SOlen').array('__infos', { + type: this.StructOrderRawParser(), + length: '__SOlen', + formatter: (x: any): StructOrder[] => + x.map((y: any) => { + const sorders = { + StructName: this.TableLookup(y.StructName), + OrderName: this.TableLookup(y.OrderName), + Names: y.Names.map((z: any) => this.TableLookup(z)), + } as StructOrder; + return sorders; + }), + }); + + private readonly _doParseDebugInfo = () => { + return new Parser() + .uint64('ModificationTime') + .nest('FunctionInfos', { + type: this.FunctionInfosParser(), + formatter: (x: any): FunctionInfo[] => x.__infos, + }) + .nest('PropertyGroups', { + type: this.PropertyGroupsParser(), + formatter: (x: any): PropertyGroup[] => x.__infos, + }) + .choice('StructOrders', { + tag: () => { + const val = this.endianness === 'little' ? 1 : 0; + return val; + }, + choices: { + 0: new Parser().skip(0), + 1: this.StructOrdersParser(), + }, + formatter: (x: any): StructOrder[] | undefined => { + if (this.endianness === 'little' && x) { + return x.__infos; + } + return undefined; + }, + }); + }; + + private readonly ParseDebugInfo = () => + new Parser().uint8('HasDebugInfo').choice('DebugInfo', { + tag: 'HasDebugInfo', + choices: { + 0: new Parser().skip(0), + 1: this._doParseDebugInfo(), + }, + formatter: (x: any): DebugInfo | undefined => { + if (!x) { + return undefined; + } + return x; + }, + }); + + private readonly _debugInfoNest = { + type: this.ParseDebugInfo(), + formatter: (x: any): DebugInfo | undefined => (x ? x.DebugInfo : undefined), + }; + + private readonly HeaderParser = () => + new Parser() + .uint32('MagicNumber') + .uint8('MajorVersion') + .uint8('MinorVersion') + .uint16('GameID') + .uint64('CompileTime') + .nest('SourceFileName', this._strNest) + .nest('UserName', this._strNest) + .nest('ComputerName', this._strNest); + + private readonly _HeaderNest = (endianness: 'little' | 'big') => { + return { + type: this.HeaderParser(), + formatter: (x: any): PexHeader => { + return { + Game: getGameFromEndianness(endianness), + MajorVersion: x.MajorVersion, + MinorVersion: x.MinorVersion, + GameID: x.GameID, + CompileTime: x.CompileTime, + SourceFileName: x.SourceFileName, + UserName: x.UserName, + ComputerName: x.ComputerName, + }; + }, + }; + }; + + private ReadPexBinary(buffer: Buffer): PexBinary | undefined { + const endianness: 'little' | 'big' | undefined = DetermineEndianness(buffer); + if (!endianness) { + return undefined; + } + const Pex = new Parser() + .endianess(endianness) + .nest('Header', this._HeaderNest(endianness)) + .nest('StringTable', this._strTableNest) + .nest('DebugInfo', this._debugInfoNest) + .parse(buffer); + + return { + Path: this.path, + Header: Pex.Header, + DebugInfo: Pex.DebugInfo, + }; } - let endianness: 'little' | 'big' | undefined = DetermineEndianness(buffer); - if (!endianness) { - return undefined; + + private ReadHeader(buffer: Buffer) { + return new Parser().endianess(this.endianness).nest('Header', this._HeaderNest(this.endianness)).parse(buffer); + } + + public async ReadPexHeader(): Promise { + // read the binary file from the path into a byte buffer + if (!fs.existsSync(this.path) || !fs.lstatSync(this.path).isFile()) { + return undefined; + } + + const buffer = fs.readFileSync(this.path); + if (!buffer || buffer.length < 4) { + return undefined; + } + const endianness: 'little' | 'big' | undefined = DetermineEndianness(buffer); + if (!endianness) { + return undefined; + } + this.endianness = endianness; + + return this.ReadHeader(buffer); + } + // not complete + async ReadPex(): Promise { + const buffer = fs.readFileSync(this.path); + if (!buffer || buffer.length < 4) { + return undefined; + } + const endianness: 'little' | 'big' | undefined = DetermineEndianness(buffer); + if (!endianness) { + return undefined; + } + this.endianness = endianness; + return this.ReadPexBinary(buffer); } - this.endianness = endianness; - return this.ReadPexBinary(buffer); - } } // returns the 64-bit timestamp of when the pex file was compiled @@ -419,7 +422,6 @@ export async function GetCompiledTime(path: string): Promise { // 'F:\\workspace\\skyrim-mod-workspace\\papyrus-lang\\src\\papyrus-lang-vscode\\_wetbpautoadjust.pex' // ); - // pexreader.ReadPex().then((pex) => { // console.log(pex); // console.log('done'); diff --git a/src/papyrus-lang-vscode/src/features/LanguageServiceStatusItems.ts b/src/papyrus-lang-vscode/src/features/LanguageServiceStatusItems.ts index 39b47bc1..51ce6c9f 100644 --- a/src/papyrus-lang-vscode/src/features/LanguageServiceStatusItems.ts +++ b/src/papyrus-lang-vscode/src/features/LanguageServiceStatusItems.ts @@ -1,6 +1,6 @@ import { ILanguageClientManager } from '../server/LanguageClientManager'; import { StatusBarItem, Disposable, window, StatusBarAlignment, TextEditor } from 'vscode'; -import { PapyrusGame, getGames, getShortDisplayNameForGame, getDisplayNameForGame } from "../PapyrusGame"; +import { PapyrusGame, getGames, getShortDisplayNameForGame, getDisplayNameForGame } from '../PapyrusGame'; import { Observable, Unsubscribable, combineLatest as combineLatest, ObservableInput, ObservedValueOf } from 'rxjs'; import { ILanguageClientHost, ClientHostStatus } from '../server/LanguageClientHost'; import { mergeMap, shareReplay } from 'rxjs/operators'; diff --git a/src/papyrus-lang-vscode/src/features/PyroTaskProvider.ts b/src/papyrus-lang-vscode/src/features/PyroTaskProvider.ts index 51619a75..c18a8cfe 100644 --- a/src/papyrus-lang-vscode/src/features/PyroTaskProvider.ts +++ b/src/papyrus-lang-vscode/src/features/PyroTaskProvider.ts @@ -14,8 +14,8 @@ import { import { CancellationToken, Disposable } from 'vscode-jsonrpc'; import { IPyroTaskDefinition, TaskOf, PyroGameToPapyrusGame } from './PyroTaskDefinition'; -import { getWorkspaceGameFromProjects, getWorkspaceGame } from '../WorkspaceGame'; -import { PapyrusGame } from "../PapyrusGame"; +import { getWorkspaceGameFromProjects } from '../WorkspaceGame'; +import { PapyrusGame } from '../PapyrusGame'; import { IPathResolver, PathResolver, pathToOsPath } from '../common/PathResolver'; import { inject, injectable } from 'inversify'; diff --git a/src/papyrus-lang-vscode/src/features/commands/AttachDebuggerCommand.ts b/src/papyrus-lang-vscode/src/features/commands/AttachDebuggerCommand.ts index 54917743..d7d342f7 100644 --- a/src/papyrus-lang-vscode/src/features/commands/AttachDebuggerCommand.ts +++ b/src/papyrus-lang-vscode/src/features/commands/AttachDebuggerCommand.ts @@ -1,7 +1,7 @@ import { injectable } from 'inversify'; import { debug } from 'vscode'; import { IPapyrusDebugConfiguration } from '../../debugger/PapyrusDebugSession'; -import { PapyrusGame, getShortDisplayNameForGame } from "../../PapyrusGame"; +import { PapyrusGame, getShortDisplayNameForGame } from '../../PapyrusGame'; import { GameCommandBase } from './GameCommandBase'; @injectable() diff --git a/src/papyrus-lang-vscode/src/features/commands/GameCommandBase.ts b/src/papyrus-lang-vscode/src/features/commands/GameCommandBase.ts index 91c09c3d..f11b0b1f 100644 --- a/src/papyrus-lang-vscode/src/features/commands/GameCommandBase.ts +++ b/src/papyrus-lang-vscode/src/features/commands/GameCommandBase.ts @@ -1,6 +1,6 @@ import { injectable, unmanaged } from 'inversify'; import { commands, Disposable } from 'vscode'; -import { PapyrusGame, getGames } from "../../PapyrusGame"; +import { PapyrusGame, getGames } from '../../PapyrusGame'; @injectable() // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/papyrus-lang-vscode/src/features/commands/GenerateProjectCommand.ts b/src/papyrus-lang-vscode/src/features/commands/GenerateProjectCommand.ts index 54b2d93a..34c20502 100644 --- a/src/papyrus-lang-vscode/src/features/commands/GenerateProjectCommand.ts +++ b/src/papyrus-lang-vscode/src/features/commands/GenerateProjectCommand.ts @@ -1,7 +1,7 @@ import { window, Uri, ExtensionContext } from 'vscode'; import { IExtensionContext } from '../../common/vscode/IocDecorators'; import { GameCommandBase } from './GameCommandBase'; -import { PapyrusGame } from "../../PapyrusGame"; +import { PapyrusGame } from '../../PapyrusGame'; import { IPathResolver } from '../../common/PathResolver'; import { copyAndFillTemplate, mkDirByPathSync } from '../../Utilities'; diff --git a/src/papyrus-lang-vscode/src/features/commands/InstallDebugSupportCommand.ts b/src/papyrus-lang-vscode/src/features/commands/InstallDebugSupportCommand.ts index 965991f3..29ea0862 100644 --- a/src/papyrus-lang-vscode/src/features/commands/InstallDebugSupportCommand.ts +++ b/src/papyrus-lang-vscode/src/features/commands/InstallDebugSupportCommand.ts @@ -5,7 +5,7 @@ import { GameCommandBase } from './GameCommandBase'; import { getGameIsRunning } from '../../Utilities'; import { waitWhile } from '../../VsCodeUtilities'; import { inject, injectable } from 'inversify'; -import { IMO2ConfiguratorService, MO2ConfiguratorService } from '../../debugger/MO2ConfiguratorService'; +import { IMO2ConfiguratorService } from '../../debugger/MO2ConfiguratorService'; import { IMO2LauncherDescriptor } from '../../debugger/MO2LaunchDescriptorFactory'; export function showGameDisabledMessage(game: PapyrusGame) { @@ -35,17 +35,17 @@ export class InstallDebugSupportCommand extends GameCommandBase { this._installer = installer; this._mo2ConfiguratorService = mo2ConfiguratorService; } - + // TODO: Fix the args protected getLauncherDescriptor(...args: [any | undefined]): IMO2LauncherDescriptor | undefined { // If we have args, it's a debugger launch. if (args.length > 0) { // args 0 indicates the launch type - let launchArgs: any[] = args[0]; + const launchArgs: any[] = args[0]; if (launchArgs.length < 1) { return; } - let launchType = launchArgs[0] as string; + const launchType = launchArgs[0] as string; if (launchType === 'XSE') { // do stuff } @@ -57,7 +57,7 @@ export class InstallDebugSupportCommand extends GameCommandBase { } protected async onExecute(game: PapyrusGame, ...args: [any | undefined]) { - let launcherDescriptor = this.getLauncherDescriptor(...args); + const launcherDescriptor = this.getLauncherDescriptor(...args); const installed = await window.withProgress( { cancellable: true, @@ -86,9 +86,9 @@ export class InstallDebugSupportCommand extends GameCommandBase { return false; } - return launcherDescriptor ? - await this._mo2ConfiguratorService.fixDebuggerConfiguration(launcherDescriptor, token) : - await this._installer.installPlugin(game, token); + return launcherDescriptor + ? await this._mo2ConfiguratorService.fixDebuggerConfiguration(launcherDescriptor, token) + : await this._installer.installPlugin(game, token); } catch (error) { window.showErrorMessage( `Failed to install Papyrus debugger support for ${getDisplayNameForGame(game)}: ${error}` diff --git a/src/papyrus-lang-vscode/src/features/commands/SearchCreationKitWikiCommand.ts b/src/papyrus-lang-vscode/src/features/commands/SearchCreationKitWikiCommand.ts index 9b7f1adf..66f56adf 100644 --- a/src/papyrus-lang-vscode/src/features/commands/SearchCreationKitWikiCommand.ts +++ b/src/papyrus-lang-vscode/src/features/commands/SearchCreationKitWikiCommand.ts @@ -1,7 +1,7 @@ import { EditorCommandBase } from '../../common/vscode/commands/EditorCommandBase'; import { TextEditor, env, Uri, window } from 'vscode'; import { ILanguageClientManager } from '../../server/LanguageClientManager'; -import { PapyrusGame } from "../../PapyrusGame"; +import { PapyrusGame } from '../../PapyrusGame'; import { inject, injectable } from 'inversify'; @injectable() diff --git a/src/papyrus-lang-vscode/src/features/projects/ProjectsTreeDataProvider.ts b/src/papyrus-lang-vscode/src/features/projects/ProjectsTreeDataProvider.ts index be47519a..726dcb89 100644 --- a/src/papyrus-lang-vscode/src/features/projects/ProjectsTreeDataProvider.ts +++ b/src/papyrus-lang-vscode/src/features/projects/ProjectsTreeDataProvider.ts @@ -10,7 +10,7 @@ import { ProjectInfoSourceInclude, ProjectInfoScript, } from '../../server/messages/ProjectInfos'; -import { PapyrusGame, getShortDisplayNameForGame } from "../../PapyrusGame"; +import { PapyrusGame, getShortDisplayNameForGame } from '../../PapyrusGame'; import { flatten } from '../../Utilities'; import { IExtensionContext } from '../../common/vscode/IocDecorators'; import { inject, injectable } from 'inversify'; diff --git a/src/papyrus-lang-vscode/src/server/LanguageClient.ts b/src/papyrus-lang-vscode/src/server/LanguageClient.ts index fd78c3b5..4b9b815d 100644 --- a/src/papyrus-lang-vscode/src/server/LanguageClient.ts +++ b/src/papyrus-lang-vscode/src/server/LanguageClient.ts @@ -3,7 +3,7 @@ import { workspace, FileSystemWatcher, OutputChannel } from 'vscode'; import { DocumentScriptInfo, documentScriptInfoRequestType } from './messages/DocumentScriptInfo'; import { DocumentSyntaxTree, documentSyntaxTreeRequestType } from './messages/DocumentSyntaxTree'; -import { PapyrusGame } from "../PapyrusGame"; +import { PapyrusGame } from '../PapyrusGame'; import { toCommandLineArgs } from '../Utilities'; import { ProjectInfos, projectInfosRequestType } from './messages/ProjectInfos'; import { Observable, BehaviorSubject } from 'rxjs'; diff --git a/src/papyrus-lang-vscode/src/server/LanguageClientHost.ts b/src/papyrus-lang-vscode/src/server/LanguageClientHost.ts index 3a03eb1d..e12beb41 100644 --- a/src/papyrus-lang-vscode/src/server/LanguageClientHost.ts +++ b/src/papyrus-lang-vscode/src/server/LanguageClientHost.ts @@ -1,12 +1,12 @@ import { Disposable, OutputChannel, window, TextDocument } from 'vscode'; import { LanguageClient, ILanguageClient, IToolArguments } from './LanguageClient'; -import { PapyrusGame, getShortDisplayNameForGame, getDefaultFlagsFileNameForGame } from "../PapyrusGame"; +import { PapyrusGame, getShortDisplayNameForGame, getDefaultFlagsFileNameForGame } from '../PapyrusGame'; import { IGameConfig } from '../ExtensionConfigProvider'; import { Observable, BehaviorSubject, of } from 'rxjs'; import { ICreationKitInfo } from '../CreationKitInfoProvider'; import { DocumentScriptInfo } from './messages/DocumentScriptInfo'; -import { shareReplay, take, map, switchMap } from 'rxjs/operators'; +import { shareReplay, take, switchMap } from 'rxjs/operators'; import { IPathResolver } from '../common/PathResolver'; import { ProjectInfos } from './messages/ProjectInfos'; import { inject } from 'inversify'; diff --git a/src/papyrus-lang-vscode/src/server/LanguageClientManager.ts b/src/papyrus-lang-vscode/src/server/LanguageClientManager.ts index 41c3c2f3..96536731 100644 --- a/src/papyrus-lang-vscode/src/server/LanguageClientManager.ts +++ b/src/papyrus-lang-vscode/src/server/LanguageClientManager.ts @@ -1,6 +1,6 @@ import { inject, injectable, interfaces } from 'inversify'; import { ILanguageClient } from './LanguageClient'; -import { PapyrusGame, getGames } from "../PapyrusGame"; +import { PapyrusGame, getGames } from '../PapyrusGame'; import { Observable, Subscription, combineLatest } from 'rxjs'; import { IExtensionConfigProvider, IGameConfig } from '../ExtensionConfigProvider'; import { map, take } from 'rxjs/operators'; From 612edab22af0e64ef211329733c2116ffda86aed Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Sun, 1 Oct 2023 17:05:32 -0700 Subject: [PATCH 15/15] manual eslint fixes --- src/papyrus-lang-vscode/src/Utilities.ts | 2 +- src/papyrus-lang-vscode/src/WorkspaceGame.ts | 1 + .../src/common/GithubHelpers.ts | 1 - src/papyrus-lang-vscode/src/common/INIHelpers.ts | 1 + src/papyrus-lang-vscode/src/common/MO2Lib.ts | 6 ++++-- src/papyrus-lang-vscode/src/common/OSHelpers.ts | 4 +++- .../src/common/PathResolver.ts | 2 -- .../src/debugger/AddLibHelpers.ts | 1 + .../src/debugger/DebugLauncherService.ts | 10 ---------- .../src/debugger/GameDebugConfiguratorService.ts | 2 -- .../src/debugger/MO2ConfiguratorService.ts | 15 +++++---------- .../PapyrusDebugConfigurationProvider.ts | 16 ++++++++-------- .../src/debugger/PexParser.ts | 3 ++- .../commands/InstallDebugSupportCommand.ts | 1 + 14 files changed, 27 insertions(+), 38 deletions(-) diff --git a/src/papyrus-lang-vscode/src/Utilities.ts b/src/papyrus-lang-vscode/src/Utilities.ts index ff44c540..62ff14bc 100644 --- a/src/papyrus-lang-vscode/src/Utilities.ts +++ b/src/papyrus-lang-vscode/src/Utilities.ts @@ -51,7 +51,7 @@ export async function getGamePIDs(game: PapyrusGame): Promise> { export async function getPIDforProcessName(processName: string): Promise> { const processList = await procList(); - const thing = processList[0]; + const gameProcesses = processList.filter((p) => p.name.toLowerCase() === processName.toLowerCase()); if (gameProcesses.length === 0) { diff --git a/src/papyrus-lang-vscode/src/WorkspaceGame.ts b/src/papyrus-lang-vscode/src/WorkspaceGame.ts index 4d0d1c98..0aed9088 100644 --- a/src/papyrus-lang-vscode/src/WorkspaceGame.ts +++ b/src/papyrus-lang-vscode/src/WorkspaceGame.ts @@ -33,6 +33,7 @@ export async function getWorkspaceGameFromProjects(ppjFiles: Uri[]): Promise { const xml = await readFile(projectFile, { encoding: 'utf-8' }); // TODO: Annoying type cast here: + // eslint-disable-next-line @typescript-eslint/no-explicit-any const results = xml2js(xml, { compact: true, trim: true }) as Record; return results['PapyrusProject']['_attributes']['Game']; diff --git a/src/papyrus-lang-vscode/src/common/GithubHelpers.ts b/src/papyrus-lang-vscode/src/common/GithubHelpers.ts index c7fc389d..988d0dd8 100644 --- a/src/papyrus-lang-vscode/src/common/GithubHelpers.ts +++ b/src/papyrus-lang-vscode/src/common/GithubHelpers.ts @@ -6,7 +6,6 @@ import * as fs from 'fs'; import { promisify } from 'util'; import { CheckHashFile } from '../Utilities'; -const readdir = promisify(fs.readdir); const exists = promisify(fs.exists); export enum DownloadResult { success, diff --git a/src/papyrus-lang-vscode/src/common/INIHelpers.ts b/src/papyrus-lang-vscode/src/common/INIHelpers.ts index b7bc95d1..d5e3e74b 100644 --- a/src/papyrus-lang-vscode/src/common/INIHelpers.ts +++ b/src/papyrus-lang-vscode/src/common/INIHelpers.ts @@ -4,6 +4,7 @@ import { promisify } from 'util'; const readFile = promisify(fs.readFile); export interface INIData { + // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; } diff --git a/src/papyrus-lang-vscode/src/common/MO2Lib.ts b/src/papyrus-lang-vscode/src/common/MO2Lib.ts index 7666bdda..a93ad513 100644 --- a/src/papyrus-lang-vscode/src/common/MO2Lib.ts +++ b/src/papyrus-lang-vscode/src/common/MO2Lib.ts @@ -400,7 +400,7 @@ function ParseInstanceINI(iniPath: string, iniData: INIData, isPortable: boolean return undefined; } // TODO: We should probably pin to a specific minor version of MO2 - const version = iniData.General['version']; + const _version = iniData.General['version']; // TODO: Figure out if this is ever not set const selectedProfile = _normInistr(iniData.General['selected_profile']) || 'Default'; @@ -904,7 +904,7 @@ export function ParseMO2CmdLineArguments(normargstring: string) { * - [installedFiles].size */ -export function isKeyOfObject(key: string | number | symbol, obj: T): key is keyof T { +export function isKeyOfObject(key: string | number | symbol, obj: T): key is keyof T { return key in obj; } @@ -929,6 +929,8 @@ function ParseModMetaIni(modMetaIni: INIData): MO2ModMeta | undefined { } const general = modMetaIni.General; // check if each key in general is a key in the type MO2ModMeta + // TODO: figure out how to do this without the any + // eslint-disable-next-line @typescript-eslint/no-explicit-any const modMeta = {} as any; for (const key in general) { if (isKeyOfObject(key, general as MO2ModMeta)) { diff --git a/src/papyrus-lang-vscode/src/common/OSHelpers.ts b/src/papyrus-lang-vscode/src/common/OSHelpers.ts index ead30e64..b2705f7d 100644 --- a/src/papyrus-lang-vscode/src/common/OSHelpers.ts +++ b/src/papyrus-lang-vscode/src/common/OSHelpers.ts @@ -21,6 +21,8 @@ export async function getRegistryValueData(key: string, value: string, hive: str try { const item = await promisify(reg.get).call(reg, value); return item.value; - } catch (e) {} + } catch (e) { + /* empty */ + } return null; } diff --git a/src/papyrus-lang-vscode/src/common/PathResolver.ts b/src/papyrus-lang-vscode/src/common/PathResolver.ts index 455d389a..ee0a8870 100644 --- a/src/papyrus-lang-vscode/src/common/PathResolver.ts +++ b/src/papyrus-lang-vscode/src/common/PathResolver.ts @@ -15,8 +15,6 @@ import { PDSModName } from './constants'; import { DetermineGameVariant, FindGamePath, FindUserGamePath } from './GameHelpers'; const exists = promisify(fs.exists); -const readdir = promisify(fs.readdir); -const readFile = promisify(fs.readFile); export interface IPathResolver { // Internal paths diff --git a/src/papyrus-lang-vscode/src/debugger/AddLibHelpers.ts b/src/papyrus-lang-vscode/src/debugger/AddLibHelpers.ts index 5bd10880..7dc93426 100644 --- a/src/papyrus-lang-vscode/src/debugger/AddLibHelpers.ts +++ b/src/papyrus-lang-vscode/src/debugger/AddLibHelpers.ts @@ -97,6 +97,7 @@ export async function getLatestAddLibReleaseInfo(): Promise { const PapyrusAttach = { @@ -60,9 +60,9 @@ export class PapyrusDebugConfigurationProvider implements DebugConfigurationProv } async resolveDebugConfiguration( - folder: WorkspaceFolder | undefined, + _folder: WorkspaceFolder | undefined, debugConfiguration: IPapyrusDebugConfiguration, - token?: CancellationToken + _token?: CancellationToken ): Promise { if (debugConfiguration.game !== undefined && debugConfiguration.request !== undefined) { if (debugConfiguration.request === 'launch') { @@ -90,13 +90,13 @@ export class PapyrusDebugConfigurationProvider implements DebugConfigurationProv // substitute all the environment variables in the given string // environment variables are of the form ${env:VARIABLE_NAME} async substituteEnvVars(string: string): Promise { - const envVars = string.match(/\$\{env:([^\}]+)\}/g); + const envVars = string.match(/\$\{env:([^}]+)\}/g); if (envVars !== null) { for (const envVar of envVars) { if (envVar === undefined || envVar === null) { continue; } - const matches = envVar?.match(/\$\{env:([^\}]+)\}/); + const matches = envVar?.match(/\$\{env:([^}]+)\}/); if (matches === null || matches.length < 2) { continue; } @@ -156,9 +156,9 @@ export class PapyrusDebugConfigurationProvider implements DebugConfigurationProv } async resolveDebugConfigurationWithSubstitutedVariables( - folder: WorkspaceFolder | undefined, + _folder: WorkspaceFolder | undefined, debugConfiguration: IPapyrusDebugConfiguration, - token?: CancellationToken + _token?: CancellationToken ): Promise { if (debugConfiguration.request === 'launch' && debugConfiguration.launcherPath) { const path = await this.substituteEnvVars(debugConfiguration.launcherPath); diff --git a/src/papyrus-lang-vscode/src/debugger/PexParser.ts b/src/papyrus-lang-vscode/src/debugger/PexParser.ts index 4bb3bd95..afb5b278 100644 --- a/src/papyrus-lang-vscode/src/debugger/PexParser.ts +++ b/src/papyrus-lang-vscode/src/debugger/PexParser.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { Parser } from 'binary-parser'; import * as fs from 'fs'; import { PapyrusGame } from '../PapyrusGame'; @@ -91,7 +92,7 @@ export interface DebugInfo { } // TODO: maybe implement this -class PexIndexedString { +export class PexIndexedString { public readonly index: number; public readonly str: string; constructor(index: number, str: string) { diff --git a/src/papyrus-lang-vscode/src/features/commands/InstallDebugSupportCommand.ts b/src/papyrus-lang-vscode/src/features/commands/InstallDebugSupportCommand.ts index 29ea0862..5d9d9211 100644 --- a/src/papyrus-lang-vscode/src/features/commands/InstallDebugSupportCommand.ts +++ b/src/papyrus-lang-vscode/src/features/commands/InstallDebugSupportCommand.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { IDebugSupportInstallService, DebugSupportInstallState } from '../../debugger/DebugSupportInstallService'; import { window, ProgressLocation } from 'vscode'; import { PapyrusGame, getDisplayNameForGame } from '../../PapyrusGame';