Skip to content

Instantly share code, notes, and snippets.

@jfmherokiller
Created June 16, 2020 18:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jfmherokiller/7aef5612e29052ffce4315ce2d5caac7 to your computer and use it in GitHub Desktop.
Save jfmherokiller/7aef5612e29052ffce4315ce2d5caac7 to your computer and use it in GitHub Desktop.
steam patched satisfactory modmanager
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const path_1 = __importDefault(require("path"));
const platform_folders_1 = require("platform-folders");
const fs_1 = __importDefault(require("fs"));
const semver_1 = require("semver");
const crypto_1 = require("crypto");
const MH = __importStar(require("./modHandler"));
const SH = __importStar(require("./smlHandler"));
const BH = __importStar(require("./bootstrapperHandler"));
const manifest_1 = require("./manifest");
const utils_1 = require("./utils");
const logging_1 = require("./logging");
const errors_1 = require("./errors");
function getConfigFolderPath(configName) {
const configPath = path_1.default.join(utils_1.configFolder, configName);
utils_1.ensureExists(configPath);
return configPath;
}
exports.getConfigFolderPath = getConfigFolderPath;
const VANILLA_CONFIG_NAME = 'vanilla';
const MODDED_CONFIG_NAME = 'modded';
const DEVELOPMENT_CONFIG_NAME = 'development';
const CacheRelativePath = '.cache';
function getInstallHash(satisfactoryPath) {
return crypto_1.createHash('sha256').update(satisfactoryPath, 'utf8').digest('hex');
}
exports.getInstallHash = getInstallHash;
function getManifestFolderPath(satisfactoryPath) {
return path_1.default.join(utils_1.manifestsDir, getInstallHash(satisfactoryPath));
}
exports.getManifestFolderPath = getManifestFolderPath;
class SatisfactoryInstall {
constructor(name, version, installLocation, mainGameAppName) {
this.installLocation = installLocation;
this._manifestHandler = new manifest_1.ManifestHandler(getManifestFolderPath(installLocation));
this.name = name;
this.version = version;
this.mainGameAppName = mainGameAppName;
}
async _getInstalledMismatches(items) {
const installedSML = SH.getSMLVersion(this.installLocation);
const installedBootstrapper = BH.getBootstrapperVersion(this.installLocation);
const installedMods = await MH.getInstalledMods(SH.getModsDir(this.installLocation));
const mismatches = {
install: {},
uninstall: [],
};
if (installedSML !== items[SH.SMLModID]) {
if (!items[SH.SMLModID] || (installedSML && items[SH.SMLModID])) {
mismatches.uninstall.push(SH.SMLModID);
}
if (items[SH.SMLModID]) {
mismatches.install[SH.SMLModID] = items[SH.SMLModID];
}
}
if (installedBootstrapper !== items[BH.BootstrapperModID]) {
if (!items[BH.BootstrapperModID] || (installedBootstrapper && items[BH.BootstrapperModID])) {
mismatches.uninstall.push(BH.BootstrapperModID);
}
if (items[BH.BootstrapperModID]) {
mismatches.install[BH.BootstrapperModID] = items[BH.BootstrapperModID];
}
}
const allMods = utils_1.mergeArrays(Object.keys(items)
.filter((item) => item !== SH.SMLModID && item !== BH.BootstrapperModID), installedMods.map((mod) => mod.mod_id));
allMods.forEach((mod) => {
var _a;
const installedModVersion = (_a = installedMods
.find((installedMod) => installedMod.mod_id === mod)) === null || _a === void 0 ? void 0 : _a.version;
if (installedModVersion !== items[mod]) {
if (!items[mod] || (installedModVersion && items[mod])) {
mismatches.uninstall.push(mod);
}
if (items[mod]) {
mismatches.install[mod] = items[mod];
}
}
});
return mismatches;
}
async validateInstall() {
const items = this._manifestHandler.getItemsList();
logging_1.debug(`Items: ${JSON.stringify(items)}`);
const mismatches = await this._getInstalledMismatches(items);
logging_1.debug(`Mismatches: ${JSON.stringify(mismatches)}`);
const modsDir = SH.getModsDir(this.installLocation);
await Promise.all(mismatches.uninstall.map((id) => {
if (id !== SH.SMLModID && id !== BH.BootstrapperModID) {
if (modsDir) {
logging_1.debug(`Removing ${id} from Satisfactory install`);
return MH.uninstallMod(id, modsDir);
}
}
return Promise.resolve();
}));
if (mismatches.uninstall.includes(SH.SMLModID)) {
logging_1.debug('Removing SML from Satisfactory install');
await SH.uninstallSML(this.installLocation);
}
if (mismatches.uninstall.includes(BH.BootstrapperModID)) {
logging_1.debug('Removing Bootstrapper from Satisfactory install');
await BH.uninstallBootstrapper(this.installLocation);
}
if (mismatches.install[SH.SMLModID]) {
logging_1.debug('Copying SML to Satisfactory install');
await SH.installSML(mismatches.install[SH.SMLModID], this.installLocation);
}
if (mismatches.install[BH.BootstrapperModID]) {
logging_1.debug('Copying Bootstrapper to Satisfactory install');
await BH.installBootstrapper(mismatches.install[BH.BootstrapperModID], this.installLocation);
}
await Object.entries(mismatches.install).forEachAsync(async (modInstall) => {
const modInstallID = modInstall[0];
const modInstallVersion = modInstall[1];
if (modInstallID !== SH.SMLModID && modInstallID !== BH.BootstrapperModID) {
if (modsDir) {
logging_1.debug(`Copying ${modInstallID}@${modInstallVersion} to Satisfactory install`);
await MH.installMod(modInstallID, modInstallVersion, modsDir);
}
}
});
}
async manifestMutate(install, uninstall, update) {
if (!await SatisfactoryInstall.isGameRunning()) {
logging_1.debug(`install: [${install.map((item) => (item.version ? `${item.id}@${item.version}` : item.id)).join(', ')}], uninstall: [${uninstall.join(', ')}], update: [${update.join(', ')}]`);
const currentManifest = this._manifestHandler.readManifest();
const currentLockfile = this._manifestHandler.readLockfile();
try {
await this._manifestHandler.setSatisfactoryVersion(this.version);
await this._manifestHandler.mutate(install, uninstall, update);
await this.validateInstall();
}
catch (e) {
await this._manifestHandler.writeManifest(currentManifest);
await this._manifestHandler.writeLockfile(currentLockfile);
if (e instanceof errors_1.ModRemovedByAuthor) {
if (update.includes(e.modID)) {
update.remove(e.modID);
uninstall.push(e.modID);
logging_1.info(`Uninstalling mod ${e.modID}, it was removed from ficsit.app`);
await this.manifestMutate(install, uninstall, update);
return;
}
update.push(e.modID);
logging_1.info(`Trying to update mod ${e.modID}, the installed version was removed from ficsit.app`);
if (e.version) {
MH.removeModFromCache(e.modID, e.version);
}
await this.manifestMutate(install, uninstall, update);
return;
}
e.message = `${e.message}\nAll changes were discarded.`;
logging_1.error(e);
await this.validateInstall();
throw e;
}
}
else {
throw new errors_1.GameRunningError('Satisfactory is running. Please close it and wait until it fully shuts down.');
}
}
async loadConfig(configName) {
const currentManifest = this._manifestHandler.readManifest();
const currentLockfile = this._manifestHandler.readLockfile();
let manifest;
let lockfile;
try {
manifest = JSON.parse(fs_1.default.readFileSync(path_1.default.join(getConfigFolderPath(configName), 'manifest.json'), 'utf8'));
manifest.satisfactoryVersion = this.version;
}
catch (e) {
throw new errors_1.InvalidConfigError(`Config ${configName} is invalid`);
}
if (fs_1.default.existsSync(path_1.default.join(getConfigFolderPath(configName), `lock-${getInstallHash(this.installLocation)}.json`))) {
try {
lockfile = JSON.parse(fs_1.default.readFileSync(path_1.default.join(getConfigFolderPath(configName), `lock-${getInstallHash(this.installLocation)}.json`), 'utf8'));
}
catch (e) {
throw new errors_1.InvalidConfigError(`Config ${configName} is invalid`);
}
}
else {
lockfile = {};
}
this._manifestHandler.writeManifest(manifest);
this._manifestHandler.writeLockfile(lockfile);
try {
await this.manifestMutate([], [], []);
}
catch (e) {
// Something invalid was found. Revert and pass the error forward
this._manifestHandler.writeManifest(currentManifest);
this._manifestHandler.writeLockfile(currentLockfile);
await this.validateInstall();
throw new errors_1.InvalidConfigError(`Error while loading config: ${e}`);
}
}
async saveConfig(configName) {
if (configName.toLowerCase() === VANILLA_CONFIG_NAME) {
throw new errors_1.InvalidConfigError('Cannot modify vanilla config. Use Modded config or create a new config');
}
const manifest = this._manifestHandler.readManifest();
delete manifest.satisfactoryVersion;
fs_1.default.writeFileSync(path_1.default.join(getConfigFolderPath(configName), 'manifest.json'), JSON.stringify(manifest));
fs_1.default.writeFileSync(path_1.default.join(getConfigFolderPath(configName), `lock-${getInstallHash(this.installLocation)}.json`), JSON.stringify(this._manifestHandler.readLockfile()));
}
async _installItem(id, version) {
return this.manifestMutate([{ id, version }], [], []);
}
async _uninstallItem(item) {
return this.manifestMutate([], [item], []);
}
async _updateItem(item) {
return this.manifestMutate([], [], [item]);
}
async installMod(modID, version) {
var _a;
if (!(await this._getInstalledMods()).some((mod) => mod.mod_id === modID)) {
logging_1.info(`Installing ${modID}${version ? `@${version}` : ''}`);
await this._installItem(modID, version);
}
else {
logging_1.info(`${modID} is already installed with version ${(_a = (await this._getInstalledMods()).find((mod) => mod.mod_id === modID)) === null || _a === void 0 ? void 0 : _a.version}`);
}
}
async installFicsitAppMod(modVersion) {
return this.installMod(modVersion.mod_id);
}
async uninstallMod(modID) {
logging_1.info(`Uninstalling ${modID}`);
return this._uninstallItem(modID);
}
async uninstallFicsitAppMod(mod) {
return this.uninstallMod(mod.id);
}
async updateMod(modID) {
logging_1.info(`Updating ${modID}`);
await this._updateItem(modID);
}
async updateFicsitAppMod(mod) {
return this.updateMod(mod.id);
}
async _getInstalledMods() {
return MH.getInstalledMods(SH.getModsDir(this.installLocation));
}
get mods() {
return utils_1.filterObject(this._manifestHandler.getItemsList(), (id) => id !== SH.SMLModID && id !== BH.BootstrapperModID);
}
get manifestMods() {
return this._manifestHandler.readManifest().items
.filter((item) => item.id !== SH.SMLModID && item.id !== BH.BootstrapperModID)
.map((item) => item.id);
}
async installSML(version) {
return this._installItem(SH.SMLModID, version);
}
async uninstallSML() {
return this._uninstallItem(SH.SMLModID);
}
async updateSML() {
logging_1.info('Updating SML to latest version');
await this._updateItem(SH.SMLModID);
}
async _getInstalledSMLVersion() {
return SH.getSMLVersion(this.installLocation);
}
get smlVersion() {
return this._manifestHandler.getItemsList()[SH.SMLModID];
}
get isSMLInstalledDev() {
return this._manifestHandler.readManifest().items.some((item) => item.id === SH.SMLModID);
}
async updateBootstrapper() {
logging_1.info('Updating bootstrapper to latest version');
await this._updateItem(BH.BootstrapperModID);
}
async clearCache() {
if (!await SatisfactoryInstall.isGameRunning()) {
MH.clearCache();
utils_1.deleteFolderRecursive(path_1.default.join(this.installLocation, CacheRelativePath));
}
else {
throw new errors_1.GameRunningError('Satisfactory is running. Please close it and wait until it fully shuts down.');
}
}
static isGameRunning() {
return utils_1.isRunning('FactoryGame-Win64-Shipping.exe'); // TODO: cross platform
}
get bootstrapperVersion() {
return this._manifestHandler.getItemsList()[BH.BootstrapperModID];
}
async _getInstalledBootstrapperVersion() {
return BH.getBootstrapperVersion(this.installLocation);
}
get launchPath() {
return `steam://rungameid/526870`;
}
get binariesDir() {
return path_1.default.join(this.installLocation, 'FactoryGame', 'Binaries', 'Win64'); // TODO: other platforms
}
get displayName() {
return `${this.name} (${this.version})`;
}
get modsDir() {
return SH.getModsDir(this.installLocation);
}
}
exports.SatisfactoryInstall = SatisfactoryInstall;
function getConfigs() {
return utils_1.dirs(utils_1.configFolder).sort();
}
exports.getConfigs = getConfigs;
function deleteConfig(name) {
if (name.toLowerCase() === VANILLA_CONFIG_NAME || name.toLowerCase() === MODDED_CONFIG_NAME || name.toLowerCase() === DEVELOPMENT_CONFIG_NAME) {
throw new errors_1.InvalidConfigError(`Cannot delete ${name} config (it is part of the default set of configs)`);
}
if (fs_1.default.existsSync(getConfigFolderPath(name))) {
utils_1.deleteFolderRecursive(getConfigFolderPath(name));
}
}
exports.deleteConfig = deleteConfig;
if (!fs_1.default.existsSync(path_1.default.join(getConfigFolderPath(VANILLA_CONFIG_NAME), 'manifest.json'))) {
fs_1.default.writeFileSync(path_1.default.join(getConfigFolderPath(VANILLA_CONFIG_NAME), 'manifest.json'), JSON.stringify({ items: new Array() }));
}
if (!fs_1.default.existsSync(path_1.default.join(getConfigFolderPath(VANILLA_CONFIG_NAME), 'lock.json'))) {
fs_1.default.writeFileSync(path_1.default.join(getConfigFolderPath(VANILLA_CONFIG_NAME), 'lock.json'), JSON.stringify({}));
}
if (!fs_1.default.existsSync(path_1.default.join(getConfigFolderPath(MODDED_CONFIG_NAME), 'manifest.json'))) {
fs_1.default.writeFileSync(path_1.default.join(getConfigFolderPath(MODDED_CONFIG_NAME), 'manifest.json'), JSON.stringify({ items: new Array() }));
}
if (!fs_1.default.existsSync(path_1.default.join(getConfigFolderPath(MODDED_CONFIG_NAME), 'lock.json'))) {
fs_1.default.writeFileSync(path_1.default.join(getConfigFolderPath(MODDED_CONFIG_NAME), 'lock.json'), JSON.stringify({}));
}
if (!fs_1.default.existsSync(path_1.default.join(getConfigFolderPath(DEVELOPMENT_CONFIG_NAME), 'manifest.json'))) {
fs_1.default.writeFileSync(path_1.default.join(getConfigFolderPath(DEVELOPMENT_CONFIG_NAME), 'manifest.json'), JSON.stringify({ items: [{ id: SH.SMLModID }] }));
}
if (!fs_1.default.existsSync(path_1.default.join(getConfigFolderPath(DEVELOPMENT_CONFIG_NAME), 'lock.json'))) {
fs_1.default.writeFileSync(path_1.default.join(getConfigFolderPath(DEVELOPMENT_CONFIG_NAME), 'lock.json'), JSON.stringify({}));
}
const EpicManifestsFolder = path_1.default.join(platform_folders_1.getDataFolders()[0], 'Epic', 'EpicGamesLauncher', 'Data', 'Manifests'); // TODO: other platforms
const UEInstalledManifest = path_1.default.join(platform_folders_1.getDataFolders()[0], 'Epic', 'UnrealEngineLauncher', 'LauncherInstalled.dat'); // TODO: other platforms
const MyPath = "D:\\SteamLibrary\\steamapps\\common\\Satisfactory\\";
async function getInstalls() {
let foundInstalls = new Array();
if (fs_1.default.existsSync(EpicManifestsFolder)) {
fs_1.default.readdirSync(EpicManifestsFolder).forEach((fileName) => {
if (fileName.endsWith('.item')) {
const filePath = path_1.default.join(EpicManifestsFolder, fileName);
try {
const jsonString = fs_1.default.readFileSync(filePath, 'utf8');
const manifest = JSON.parse(jsonString);
if (manifest.CatalogNamespace === 'crab') {
try {
const gameManifestString = fs_1.default.readFileSync(path_1.default.join(manifest.ManifestLocation, `${manifest.InstallationGuid}.mancpn`), 'utf8');
const gameManifest = JSON.parse(gameManifestString);
if (gameManifest.AppName === manifest.MainGameAppName
&& gameManifest.CatalogItemId === manifest.CatalogItemId
&& gameManifest.CatalogNamespace === manifest.CatalogNamespace) {
const installWithSamePath = foundInstalls.find((install) => install.installLocation === manifest.InstallLocation);
if (installWithSamePath) {
if (parseInt(manifest.AppVersionString, 10) > parseInt(installWithSamePath.version, 10)) {
installWithSamePath.version = manifest.AppVersionString;
}
}
else {
foundInstalls.push(new SatisfactoryInstall(manifest.DisplayName, manifest.AppVersionString, manifest.InstallLocation, manifest.MainGameAppName));
}
}
else {
logging_1.warn(`Epic install info points to invalid folder ${manifest.InstallLocation}. If you moved your install to an external drive, try verifying the game in Epic and restarting your PC.`);
}
}
catch (e) {
logging_1.warn(`Epic install info points to invalid folder ${manifest.InstallLocation}. If you moved your install to an external drive, try verifying the game in Epic and restarting your PC.`);
}
}
}
catch (e) {
logging_1.info(`Found invalid manifest: ${fileName}`);
}
}
});
}
if (foundInstalls.length === 0) {
logging_1.warn('No Satisfactory installs found');
if(fs_1.default.existsSync(MyPath))
{
foundInstalls.push(new SatisfactoryInstall("Default","124884",MyPath,"test"));
}
}
let installedManifest = { InstallationList: [] };
if (fs_1.default.existsSync(UEInstalledManifest)) {
try {
installedManifest = JSON.parse(fs_1.default.readFileSync(UEInstalledManifest, 'utf8'));
//foundInstalls = foundInstalls.filter((install) => installedManifest.InstallationList.some((manifestInstall) => manifestInstall.InstallLocation === install.installLocation)); // Filter out old .items left over by Epic
if (foundInstalls.length === 0) {
logging_1.warn('UE manifest filtered all installs.');
}
}
catch (e) {
logging_1.info('Invalid UE manifest. The game might appear multiple times.');
}
}
else {
logging_1.info('Invalid UE manifest. The game might appear multiple times.');
}
foundInstalls.sort((a, b) => {
const semverCmp = semver_1.compare(semver_1.valid(semver_1.coerce(a.version)) || '0.0.0', semver_1.valid(semver_1.coerce(b.version)) || '0.0.0');
if (semverCmp === 0) {
return a.name.localeCompare(b.name);
}
return semverCmp;
});
return foundInstalls;
}
exports.getInstalls = getInstalls;
//# sourceMappingURL=satisfactoryInstall.js.map
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment