Skip to content

Commit 6b28d60

Browse files
committed
0.13.3:
- Export profile now exports to a 7Z archive instead of directly to a folder. - Import profile can now import from a folder or file archive. - Updated README with documentation for profile importing/exporting. - Added OBSE detection for Oblivion Remastered.
1 parent 9fe363c commit 6b28d60

File tree

7 files changed

+176
-70
lines changed

7 files changed

+176
-70
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ To install Stellar, simply download the latest release from the [releases page](
5757
> * [**Profile path overrides**](#profile-path-overrides)
5858
> * [**Steam compat symlinks (Linux)**](#linux-steam-compat-symlinks)
5959
> * [**Profile locking**](#profile-locking)
60+
> * [**External profiles**](#add-external-profiles)
61+
> * [**Import profiles**](#import-profiles)
62+
> * [**Export profiles**](#export-profiles)
63+
> * [**Delete profiles**](#delete-profiles)
6064
> * [**Base profiles**](#base-profiles)
6165
> * **Mods**
6266
> * [**Adding mods**](#add-some-mods)
@@ -196,6 +200,22 @@ Profiles can be locked/unlocked by clicking the "Lock Profile" button at the top
196200

197201
You can create additional profiles at any time by pressing the **Create Profile** button above the **Mod List** section or by selecting **Profile > Add New Profile** from the the menu bar.
198202

203+
### Add external profiles
204+
205+
External profiles can be added to SML from **Profile > Add External Profile**. An external profile will remain in the same directory it was added from, allowing for profiles to be used from any location.
206+
207+
### Import profiles
208+
209+
Existing profiles can be imported into SML from **Profile > Import Profile**.
210+
211+
### Export profiles
212+
213+
You can export a profile from **Profile > Export Profile**. Exported profiles will be removed from SML after successful export. A backup of the profile will be written to `<OS temp dir>/SML` before export.
214+
215+
### Delete profiles
216+
217+
You can delete a profile from **Profile > Delete Profile**. **This cannot be undone**.
218+
199219
**Tip:** You can change the app theme at any time under **File > Preferences**.
200220

201221
## Base profiles

game-db.json

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@
130130
}
131131
],
132132
"multipleDataRoots": true,
133-
"gameBinary": ["OblivionRemastered.exe"],
133+
"gameBinary": ["OblivionRemastered.exe", "OblivionRemastered/Binaries/Win64/obse64_loader.exe"],
134134
"pluginListType": "Gamebryo",
135135
"pluginFormats": ["esm", "esp"],
136136
"pluginDataRoot": "OblivionRemastered/Content/Dev/ObvData/Data",
@@ -236,15 +236,24 @@
236236
"Wwise.ini"
237237
],
238238
"scriptExtenders": [
239+
{
240+
"name": "OBSE64",
241+
"binaries": [
242+
"OblivionRemastered/Binaries/Win64/obse64_loader.exe"
243+
],
244+
"modPaths": [
245+
"OblivionRemastered/Content/Dev/ObvData/Data/OBSE"
246+
]
247+
},
239248
{
240249
"name": "UE4SS",
241250
"binaries": [
242251
"OblivionRemastered/Binaries/Win64/ue4ss/UE4SS.dll",
243252
"OblivionRemastered/Binaries/WinGDK/ue4ss/UE4SS.dll"
244253
],
245254
"modPaths": [
246-
"OblivionRemastered/Binaries/Win64/Mods/Scripts",
247-
"OblivionRemastered/Binaries/WinGDK/Mods/Scripts"
255+
"OblivionRemastered/Binaries/Win64/ue4ss/Mods",
256+
"OblivionRemastered/Binaries/WinGDK/ue4ss/Mods"
248257
]
249258
}
250259
],

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"author": "Mychal Thompson <mychal.r.thompson@gmail.com>",
44
"repository": "https://github.com/lVlyke/stellar-mod-loader",
55
"license": "GPL-3.0",
6-
"version": "0.13.2",
6+
"version": "0.13.3",
77
"main": "electron.js",
88
"scripts": {
99
"app:build-dist-debug": "npm run build && npm run package",

src/app/models/app-message.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ export namespace AppMessage {
162162
id: `${Prefix}:loadExternalProfile`;
163163
data: {
164164
profilePath?: string;
165+
directImport?: boolean;
165166
};
166167
result?: AppProfile;
167168
}

src/app/services/profile-manager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -722,7 +722,7 @@ export class ProfileManager {
722722

723723
public importProfileFromUser(directImport: boolean = false): Observable<AppProfile | undefined> {
724724
// Load the external profile
725-
return runOnce(ElectronUtils.invoke("app:loadExternalProfile", {}).pipe(
725+
return runOnce(ElectronUtils.invoke("app:loadExternalProfile", { directImport }).pipe(
726726
switchMap((profile) => {
727727
if (profile) {
728728
const profileName = last(profile.name.split(/[\\/]/)) ?? "Imported Profile";

src/electron.js

Lines changed: 139 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ class ElectronLoader {
8585
"issues": `${ElectronLoader.APP_PACKAGE.repository}/issues`,
8686
"paypal_donation": "https://paypal.me/lVlyke"
8787
};
88+
static /** @type {string} */ APP_TMP_DIR = path.resolve(path.join(os.tmpdir(), "SML"));
8889
static /** @type {number} */ GAME_SCHEMA_VERSION = 1.1;
8990
static /** @type {string} */ GAME_DB_FILE = path.join(__dirname, "game-db.json");
9091
static /** @type {string} */ GAME_RESOURCES_DIR = path.join(__dirname, "resources");
@@ -382,19 +383,69 @@ class ElectronLoader {
382383

383384
ipcMain.handle("app:loadExternalProfile", async (
384385
_event,
385-
/** @type {import("./app/models/app-message").AppMessageData<"app:loadExternalProfile">} */ { profilePath }
386+
/** @type {import("./app/models/app-message").AppMessageData<"app:loadExternalProfile">} */ { profilePath, directImport }
386387
) => {
387388
if (!profilePath) {
389+
const allowedExtensions = ["json"];
390+
391+
// Allow importing from archive if not doing a direct import
392+
if (!directImport) {
393+
allowedExtensions.push("7z", "7zip", "zip", "rar")
394+
}
395+
388396
const pickedFile = (await dialog.showOpenDialog({
389-
properties: ["openDirectory"]
397+
properties: ["openFile"],
398+
filters: [{
399+
name: "SML Profile",
400+
extensions: allowedExtensions
401+
}]
390402
}));
391403

392404
profilePath = pickedFile?.filePaths[0];
393405
}
394406

407+
if (!profilePath) {
408+
return null;
409+
}
410+
411+
// If profile is uncompressed, use the dirname
412+
if (profilePath.endsWith(".json")) {
413+
profilePath = path.dirname(profilePath);
414+
} else {
415+
const archivePath = profilePath;
416+
const profileName = path.basename(archivePath, path.extname(archivePath));
417+
418+
try {
419+
const stagingDir = path.resolve(path.join(ElectronLoader.APP_TMP_DIR, profileName));
420+
const _7zBinaryPath = this.#resolve7zBinaryPath();
421+
422+
// Clean the tmp staging dir
423+
await fs.remove(stagingDir);
424+
425+
// Decompress profile to staging dir
426+
await new Promise((resolve, reject) => {
427+
const decompressStream = Seven.extractFull(archivePath, stagingDir, { $bin: _7zBinaryPath });
428+
decompressStream.on("end", () => resolve(true));
429+
decompressStream.on("error", (e) => reject(e));
430+
});
431+
432+
profilePath = stagingDir;
433+
} catch (e) {
434+
log.error("Failed to load external profile from path:", profilePath, e);
435+
return null;
436+
}
437+
}
438+
439+
// Attempt to load the profile
395440
if (profilePath) {
396441
profilePath = path.resolve(profilePath); // Make sure path is absolute
397-
return this.loadProfileFromPath(profilePath, profilePath);
442+
const loadedProfile = this.loadProfileFromPath(profilePath, profilePath);
443+
444+
if (!loadedProfile) {
445+
log.error("Failed to load external profile from path:", profilePath);
446+
}
447+
448+
return loadedProfile;
398449
}
399450
});
400451

@@ -412,34 +463,52 @@ class ElectronLoader {
412463
const profileDir = this.getProfileDir(profile);
413464
const defaultProfileDir = this.getDefaultProfileDir(profile.name);
414465

415-
if (profileDir === defaultProfileDir) {
416-
/** @type { string | undefined } */ let exportFolder = undefined;
466+
// Choose path to save profile archive
467+
const exportFilePath = (await dialog.showSaveDialog({
468+
defaultPath: profile.name,
469+
filters: [{
470+
name: "Exported Profile",
471+
extensions: ["7z"]
472+
}]
473+
}))?.filePath;
417474

418-
// Pick a path that isn't in the profiles directory
419-
do {
420-
const exportFolderPick = (await dialog.showOpenDialog({
421-
properties: ["openDirectory"]
422-
}));
423-
424-
exportFolder = exportFolderPick?.filePaths[0];
425-
} while (exportFolder && path.resolve(exportFolder).startsWith(path.resolve(ElectronLoader.APP_PROFILES_DIR)));
475+
if (!exportFilePath) {
476+
return undefined;
477+
}
426478

427-
if (!exportFolder) {
428-
return undefined;
429-
}
479+
const initialCwd = process.cwd();
480+
// Compress the profile data to archive
481+
try {
482+
const _7zBinaryPath = this.#resolve7zBinaryPath();
430483

431-
// Move profile to the new folder
432-
fs.moveSync(profileDir, exportFolder, { overwrite: true });
484+
process.chdir(path.resolve(profileDir));
433485

434-
return exportFolder;
435-
} else if (fs.existsSync(defaultProfileDir)) {
436-
// If the profile is located at a non-default path, we just need to remove its symlink
437-
fs.removeSync(defaultProfileDir);
486+
await new Promise((resolve, reject) => {
487+
const compressStream = Seven.add(exportFilePath, ".", {
488+
$bin: _7zBinaryPath,
489+
recursive: true
490+
});
491+
492+
compressStream.on("end", () => resolve(true));
493+
compressStream.on("error", (e) => reject(e));
494+
});
495+
} catch (e) {
496+
log.error("Failed to export profile: ", e);
497+
return undefined;
498+
} finally {
499+
process.chdir(initialCwd);
500+
}
438501

439-
return profileDir;
502+
// Remove profile from SML and back up files to app tmp dir
503+
const backupDir = path.join(ElectronLoader.APP_TMP_DIR, `${profile.name}.bak_${this.#asFileName(new Date().toISOString())}`);
504+
await fs.move(profileDir, backupDir, { overwrite: true });
505+
506+
// Remove any symlinks to profile
507+
if (profileDir !== defaultProfileDir) {
508+
await fs.remove(defaultProfileDir);
440509
}
441510

442-
return undefined;
511+
return exportFilePath;
443512
});
444513

445514
ipcMain.handle("app:deleteProfile", async (
@@ -2322,45 +2391,7 @@ class ElectronLoader {
23222391
case ".zip":
23232392
case ".rar": {
23242393
decompressOperation = new Promise((resolve, _reject) => {
2325-
// Look for 7-Zip installed on system
2326-
const _7zBinaries = [
2327-
"7zzs",
2328-
"7zz",
2329-
"7z",
2330-
"7z.exe"
2331-
];
2332-
2333-
const _7zBinaryLocations = [
2334-
"C:\\Program Files\\7-Zip\\7z.exe",
2335-
"C:\\Program Files (x86)\\7-Zip\\7z.exe"
2336-
];
2337-
2338-
let _7zBinaryPath = _7zBinaryLocations.find(_7zPath => fs.existsSync(_7zPath));
2339-
2340-
if (!_7zBinaryPath) {
2341-
_7zBinaryPath = _7zBinaries.reduce((_7zBinaryPath, _7zBinaryPathGuess) => {
2342-
try {
2343-
const which7zBinaryPath = which.sync(_7zBinaryPathGuess);
2344-
_7zBinaryPath = (Array.isArray(which7zBinaryPath)
2345-
? which7zBinaryPath[0]
2346-
: which7zBinaryPath
2347-
) ?? undefined;
2348-
} catch (_err) {}
2349-
2350-
return _7zBinaryPath;
2351-
}, _7zBinaryPath);
2352-
}
2353-
2354-
if (!_7zBinaryPath) {
2355-
// Fall back to bundled 7-Zip binary if it's not found on system
2356-
// TODO - Warn user about opening RARs if 7-Zip not installed on machine
2357-
_7zBinaryPath = sevenBin.path7za;
2358-
2359-
log.warn("7-Zip binary was not found on this machine. Falling back to bundled binary.");
2360-
} else {
2361-
log.info("Found 7-Zip binary: ", _7zBinaryPath);
2362-
}
2363-
2394+
const _7zBinaryPath = this.#resolve7zBinaryPath();
23642395
const decompressStream = Seven.extractFull(filePath, modDirStagingPath, { $bin: _7zBinaryPath });
23652396
decompressStream.on("end", () => resolve(true));
23662397
decompressStream.on("error", (e) => {
@@ -4413,6 +4444,51 @@ class ElectronLoader {
44134444
return "";
44144445
}
44154446

4447+
/** @return {string} */
4448+
#resolve7zBinaryPath() {
4449+
// Look for 7-Zip installed on system
4450+
const _7zBinaries = [
4451+
"7zzs",
4452+
"7zz",
4453+
"7z",
4454+
"7z.exe"
4455+
];
4456+
4457+
const _7zBinaryLocations = [
4458+
"C:\\Program Files\\7-Zip\\7z.exe",
4459+
"C:\\Program Files (x86)\\7-Zip\\7z.exe"
4460+
];
4461+
4462+
let _7zBinaryPath = _7zBinaryLocations.find(_7zPath => fs.existsSync(_7zPath));
4463+
4464+
if (!_7zBinaryPath) {
4465+
_7zBinaryPath = _7zBinaries.reduce((_7zBinaryPath, _7zBinaryPathGuess) => {
4466+
try {
4467+
const which7zBinaryPath = which.sync(_7zBinaryPathGuess);
4468+
_7zBinaryPath = (Array.isArray(which7zBinaryPath)
4469+
? which7zBinaryPath[0]
4470+
: which7zBinaryPath
4471+
) ?? undefined;
4472+
} catch (_err) {}
4473+
4474+
return _7zBinaryPath;
4475+
}, _7zBinaryPath);
4476+
}
4477+
4478+
if (!_7zBinaryPath) {
4479+
// Fall back to bundled 7-Zip binary if it's not found on system
4480+
// TODO - Warn user about opening RARs if 7-Zip not installed on machine
4481+
_7zBinaryPath = sevenBin.path7za;
4482+
4483+
log.warn("7-Zip binary was not found on this machine. Falling back to bundled binary.");
4484+
log.warn("NOTE: RAR archives can not be read using the bundled binary. Install 7-Zip to read RAR archives.");
4485+
} else {
4486+
log.info("Found 7-Zip binary: ", _7zBinaryPath);
4487+
}
4488+
4489+
return _7zBinaryPath;
4490+
}
4491+
44164492
/** @return {string} */
44174493
#formatLogData(logData) {
44184494
return logData?.map(arg => this.#formatLogArg(arg)).join(" ") ?? "";

0 commit comments

Comments
 (0)