-
Notifications
You must be signed in to change notification settings - Fork 11
Creating Installers
The best way to make sure you’re doing it right is to check installers that are already created in the repo and working currently, and base yours off of them.
Here are examples of the steps to add a new installer that install a new example component named "MyComponent", with "MCO" as its internal component code.
Installers are created as JavaScript classes that respect the following contract:
- Contains a constructor.
- Contains an
async install()
method.
Here’s an example of an Installer that downloads a .zip and extracts it in a predefined directory.
// Require class dependencies.
const Modal = require( '../modal' );
const download = require( '../download' );
const unzip = require( '../unzip' );
/**
* Downloads and installs MyComponent.
* The class name must be the component’s name followed by "Installer". For example: SimitoneInstaller.
*/
class MyComponentInstaller {
/**
* Sets up the installer
*
* @param {FSOLauncher} FSOLauncher
* @param {string} path Final path to install in.
*/
constructor( FSOLauncher, path ) {
this.FSOLauncher = FSOLauncher;
// The path the user chose (coming from fsolauncher.js).
this.path = path;
// A "unique enough" identifier for this download.
this.id = Math.floor( Date.now() / 1000 );
// The temporary directory for downloaded files.
this.tempPath = `${global.appData}temp/myzip-${this.id}.zip`; // Make sure to include the id to avoid possible collisions.
// Configure the download.
this.dl = download( { from: 'http://someurl.com/somefile.zip', to: this.tempPath } );
}
/**
* Creates or updates the progress bar.
*
* @param {string} message
* @param {number} percentage
*/
createProgressItem( message, percentage ) {
this.FSOLauncher.IPC.addProgressItem(
'FSOProgressItem' + this.id, // Include the id to avoid collisions.
'My Zipped File', // The download’s name.
'Installing in' + this.path, // The download’s info text. Remains static throughout the installation.
message, // The download’s progress text. More details about the current progress.
percentage // The download’s percentage, will be rendered in the progress bar.
);
// Set the native progress bar progress as well. (The one that appears in the taskbar icon, native to the OS)
this.FSOLauncher.setProgressBar( percentage == 100 ? 2 : percentage / 100 );
}
/**
* Executes all steps in order and returns a Promise.
*
* @return {Promise}
*/
async install() {
try {
await this.step1();
await this.step2();
await this.step3();
this.end();
} catch ( err ) {
this.error( err );
throw err; // Send it back to the caller.
}
}
// Every step MUST return a Promise.
/**
* Downloads the zip into the temporary directory.
*
* @return {Promise}
*/
step1() { return this.download(); }
/**
* Makes sure the final directory exists.
*
* @return {Promise}
*/
step2() { return this.setupDir(); }
/**
* Extracts the zip into the final directory.
*
* @return {Promise}
*/
step3() { return this.extract(); }
/**
* Implement download(), setupDir() and extract().
* Base off of examples already in the repo if necessary.
* They MUST all return Promises.
*/
// ...
/**
* Cleans up and updates the progress to finished.
*/
end() {
// Do some cleanup. This is a method of the download() object.
this.dl.cleanup();
// Report the final progress (100%).
this.createProgressItem( 'Installation finished!', 100 );
// Mark the progress item as finished.
this.FSOLauncher.IPC.stopProgressItem( 'FSOProgressItem' + this.id );
}
/**
* Cleans up and updates the progress to error.
*
* @param {Error} err
*/
error( err ) {
// Do some cleanup. This is a method of the download() object.
this.dl.cleanup();
// Show a short error message in the progress item.
this.createProgressItem( 'Failed to install MyComponent.', 100 );
// Mark the progress item as finished (errored out).
this.FSOLauncher.IPC.stopProgressItem( 'FSOProgressItem' + this.id );
}
}
module.exports = MyComponentInstaller;
Use the component's code (short, singe-word, uppercase representation of the component's name) as the key. Use the same code in the next steps.
module.exports = {
'FSO': require( './fso' ),
'RMS': require( './rms' ),
'TSO': require( './tso' ),
'Simitone': require( './simitone' ),
'SDL': require( './sdl' ),
'Mono': require( './mono' ),
'MacExtras': require( './macextras' ),
'OpenAL': require( './executable' ),
'NET': require( './executable' ),
'MCO': require( './mco' ), // <-- The new component
};
In src/fsolauncher/constants.js, add the component's "pretty" name and code in components
. The code should only be a single word.
components: {
'TSO': 'The Sims Online',
'FSO': 'FreeSO',
'OpenAL': 'OpenAL',
'NET': '.NET Framework',
'RMS': 'Remesh Package',
'Simitone': 'Simitone for Windows',
'Mono': 'Mono Runtime',
'MacExtras': 'FreeSO MacExtras',
'SDL': 'SDL2',
'MCO' 'My Component' // <-- The new component
}
In the same file, modify dependency
to add any dependencies (on other components) this component has.
dependency: {
'FSO': [ 'TSO', ...( process.platform === 'darwin' ? [ 'Mono', 'SDL' ] : [ 'OpenAL' ] ) ],
'RMS': [ 'FSO' ],
'MacExtras': [ 'FSO' ],
'Simitone': ( process.platform === 'darwin' ) ? [ 'Mono', 'SDL' ] : [],
'MCO': [ 'FSO' ] // For our example, let’s say we require FreeSO to be installed.
},
If the component requires internet to be installed (you could include the binaries in the /bin/ folder, which would make the installation local-only),
you should add it to the needInternet
array, in the same file.
needInternet: [
'TSO',
'FSO',
'RMS',
'Simitone',
'Mono',
'MacExtras',
'SDL',
'MCO' // <-- The new component
],
Modify src/fsolauncher/library/registry.js to include your component in the getInstalled()
method.
Use techniques already present in the class (registry/file existence check) to check if the component is installed.
static getInstalled() {
return new Promise( ( resolve, reject ) => {
const Promises = [];
Promises.push( Registry.get( 'OpenAL', Registry.getOpenALPath() ) );
Promises.push( Registry.get( 'FSO', Registry.getFSOPath() ) );
Promises.push( Registry.get( 'TSO', Registry.getTSOPath() ) );
Promises.push( Registry.get( 'NET', Registry.getNETPath() ) );
Promises.push( Registry.get( 'Simitone', Registry.getSimitonePath() ) );
Promises.push( Registry.get( 'TS1', Registry.getTS1Path() ) );
Promises.push( Registry.get( 'MCO', Registry.getMCOPath() ) ); // Added MCO. Make sure to implement this method getMCOPath.
if( process.platform == 'darwin' ) { // These are only for macOS.
Promises.push( Registry.get( 'Mono', Registry.getMonoPath() ) );
Promises.push( Registry.get( 'SDL', Registry.getSDLPath() ) );
}
Promise.all( Promises )
.then( resolve )
.catch( reject );
} );
}
To make this Installer able to be used, you need to modify the install()
method in src/fsolauncher/fsolauncher.js.
Add the component's code to the switch in install()
:
-
handleSimpleInstall
is for installs that are installed in the FreeSO directory. For example, remeshes that are extracted there. There is no way for the user to customize the installation. -
handleStandardInstall
is for installs that ask the user for a directory to install in - this directory is then passed into the installer class. -
handleExecutableInstall
is for running.exe
s present in thebin
folder. Currently used for OpenAL.
switch ( componentCode ) {
case 'Mono':
case 'MacExtras':
case 'SDL':
case 'RMS':
display = await this.handleSimpleInstall( componentCode, options );
break;
case 'TSO':
case 'FSO':
case 'Simitone':
case 'MCO': // Since the first parameter of the new installer's constructor is a path, we put it here - it will ask the user for a folder.
display = await this.handleStandardInstall( componentCode, options );
break;
case 'OpenAL':
case 'NET':
display = await this.handleExecutableInstall( componentCode, options );
break;
If the component should be required to install FreeSO, add it to the complete installation flow. If it’s an optional component (like the RemeshPackage installer), just skip to step 7.
Edit the src/fsolauncher/library/installers/complete-installer.js file to add a new step that installs your new component.
The Complete Installer follows mostly the same structure as the installer we just created, but installs multiple components (using the FSOLauncher.install()
method) instead of just a single component.
The components in the installer screen have an image related to the component and some text.
You need to add a new component on this screen, so edit the src/fsolauncher_ui/fsolauncher.pug file.
Go to the section where the installer page is located at and add a new one.
- Notice the
install
attribute contains the component’s internal code. - Note also the inline styles which define the background image. You should add a backdrop image to the src/fsolauncher_ui/fsolauncher_images folder, and tweak it via these inline styles until it looks acceptable and the same as the other components.
- Finally, the
h1
andspan
contain the component’s name and a small tagline, respectively.
div
.item(install='MCO', style='background:url(fsolauncher_images/mco_backdrop.png) #fff; background-position:center center; background-size:80%;background-repeat:no-repeat;')
.tag
h1 #{INSTALLER_MCO_TITLE}
span #{INSTALLER_MCO_DESCR}
.tick.installed
i.material-icons done
- Make sure it looks good in Dark Mode as well! (Open Beta Dark and Halloween themes). Use the appropriate .css files in src/fsolauncher_ui/fsolauncher_styles/fsolauncher_themes to create overrides for when these themes are used.
Having followed these steps, users should be able to install the new component through the Complete Installer and individually, using the Installer page.
Home · User docs · Developer docs · FAQ