From 01b78bef4c2d1219b171b5a923de700c91f0ff6a Mon Sep 17 00:00:00 2001 From: jgaribsin Date: Wed, 10 Jan 2024 14:46:05 -0700 Subject: [PATCH] #skip-ci v0.0.7 beta candidate - installer now also updates clouds, render distance, chat height and volumes - added dedicated `options.txt` parser - added legacy minecraft installation path - logs now include installer version - split files in `src/providers/` into dedicated files for each function --- package.json | 2 +- src-electron/electron-main.ts | 5 +- src/components/CloseInstaller.vue | 3 ++ src/components/MultiplayerInstall.vue | 8 ++- src/components/SingleplayerInstall.vue | 8 ++- src/components/SocialsGrid.vue | 2 +- src/providers/DownloadFile.ts | 8 +-- src/providers/DownloadMods.ts | 51 ++++++++++++++++++ src/providers/DownloadResourcePack.ts | 33 ++++++++++++ src/providers/DownloadShards.ts | 4 +- src/providers/ExtractFile.ts | 28 ---------- src/providers/InstallFabric.ts | 72 -------------------------- src/providers/UpdateMinecraftOpts.ts | 41 +++++++++++++++ 13 files changed, 146 insertions(+), 119 deletions(-) create mode 100644 src/providers/DownloadMods.ts create mode 100644 src/providers/DownloadResourcePack.ts delete mode 100644 src/providers/ExtractFile.ts create mode 100644 src/providers/UpdateMinecraftOpts.ts diff --git a/package.json b/package.json index e88dcdd..21bc47b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "drehmal-installer", - "version": "0.0.6", + "version": "0.0.7", "description": "Drehmal, Minecraft map installer", "productName": "Drehmal Installer", "author": "majorlue", diff --git a/src-electron/electron-main.ts b/src-electron/electron-main.ts index 7d67b4c..3e9645c 100644 --- a/src-electron/electron-main.ts +++ b/src-electron/electron-main.ts @@ -6,6 +6,8 @@ const path = require('path'); const os = require('os'); const fs = require('fs'); +import { version } from '../package.json'; + // Optional, initialize the logger for any renderer process log.initialize({ spyRendererConsole: true }); const currDate = dayjs().format('DD-MM-YYYY'); @@ -34,7 +36,7 @@ log.transports.file.format = '[{h}:{i}:{s}.{ms}] [{level}] {text}'; log.errorHandler.startCatching(); log.eventLogger.startLogging(); -log.info('Logger initialised'); +log.info(`Installer v${version}: Logger initialised`); let mainWindow: BrowserWindow | undefined; @@ -46,6 +48,7 @@ function createWindow() { icon: path.resolve(__dirname, 'icons/icon.png'), // tray icon width: 1000, height: 600, + resizable: false, useContentSize: true, webPreferences: { nodeIntegration: true, diff --git a/src/components/CloseInstaller.vue b/src/components/CloseInstaller.vue index cb7335e..5292483 100644 --- a/src/components/CloseInstaller.vue +++ b/src/components/CloseInstaller.vue @@ -27,7 +27,10 @@ const launchButton = ref(false); const platform: Ref = ref(); const windowsLocations: string[] = [ + // Microsoft Store install location 'C:\\XboxGames\\Minecraft Launcher\\Content\\Minecraft.exe', + // Legacy install location + 'C:\\Program Files (x86)\\Minecraft', ]; let windowsPath = ''; diff --git a/src/components/MultiplayerInstall.vue b/src/components/MultiplayerInstall.vue index 2cab0ec..bbaf114 100644 --- a/src/components/MultiplayerInstall.vue +++ b/src/components/MultiplayerInstall.vue @@ -34,11 +34,9 @@ import { storeToRefs } from 'pinia'; import ProgressBox from 'src/components/ProgressBox.vue'; import { ProgressBox as ProgressBoxType, Shard } from 'src/components/models'; -import { - downloadMods, - downloadResourcePack, - installFabric, -} from 'src/providers/InstallFabric'; +import { downloadMods } from 'src/providers/DownloadMods'; +import { downloadResourcePack } from 'src/providers/DownloadResourcePack'; +import { installFabric } from 'src/providers/InstallFabric'; import { useSourcesStore } from 'src/stores/SourcesStore'; import { useStateStore } from 'src/stores/StateStore'; import { ref } from 'vue'; diff --git a/src/components/SingleplayerInstall.vue b/src/components/SingleplayerInstall.vue index 32f4ee4..bf38159 100644 --- a/src/components/SingleplayerInstall.vue +++ b/src/components/SingleplayerInstall.vue @@ -58,12 +58,10 @@ import { Shard, ShardsBox, } from 'src/components/models'; +import { downloadMods } from 'src/providers/DownloadMods'; +import { downloadResourcePack } from 'src/providers/DownloadResourcePack'; import { downloadShards } from 'src/providers/DownloadShards'; -import { - downloadMods, - downloadResourcePack, - installFabric, -} from 'src/providers/InstallFabric'; +import { installFabric } from 'src/providers/InstallFabric'; import { useInstallerStore } from 'src/stores/InstallerStore'; import { useSourcesStore } from 'src/stores/SourcesStore'; import { useStateStore } from 'src/stores/StateStore'; diff --git a/src/components/SocialsGrid.vue b/src/components/SocialsGrid.vue index 158aad7..69efa4f 100644 --- a/src/components/SocialsGrid.vue +++ b/src/components/SocialsGrid.vue @@ -32,7 +32,7 @@ const socials: Ref = ref([ img: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAeCAYAAABuUU38AAAACXBIWXMAAAsTAAALEwEAmpwYAAACIUlEQVRYhe2YwW7aQBCGvzGWmuYApqghrxCllKrHphJS+gBIPRDlVfoAeZXIHCp4gCKQmh6rpnmJUInY4ZDkkLA9IFs23kC8No2R+p2s3Vl7fs94Zr3Sc4e0Oy363ZFiA2l3WtLvjhClFP3uSIkITvXlc/uVCt+7RSlFu9MS6blDBWBZQsXZLCHX/i2z2TyRbIDqq+1wslYrP49XKZlMpuGL965u5kICAhFbZTu5skDcTe+p1cpMJtNwLOGxiYijD9/C62jF6P74FLPrROxYYreKrbLN3fQ+NmYjqe6hpblbTYxdjP3E2DuN3W+NXWoEbMmg5PhgwAxFs550sFF3YhEQEZp1J2H3NmKXNjLhvQE7ngzpaGgci6KLwGN2vy49Yz8ArEyrC8R/IUWjMEJelLK4Isk+8hSCvqEru6bsva5kqF7KTIhOwPmlh7vgwMnnn+H1xdjn9Ozw0XnIVr1yS63SitR4UOv9S8jvG1mzo6vITcjDCh057ISWkts2t1l3OP44AGCmoCTwZscJ5xu7VY4OBqAUliVYCPs7lbwej51nRjQiji9igXavtYhJ5JQqUB8JMH2vhRNiitE38uXreyDeB87HHu5ZukYW6zN/fE6/Hy6xXk6O5Tfb8uAQwZRMVUsXmbTodgQmJCKy+C/8ZAwbhUkcdD5Kzx2qTT0OCgiPg7yrG2B+vhU1KDqB3zBPrXXvHv4FEnzsAqiowg1CAP4CkkKoohNdiXEAAAAASUVORK5CYII=', hover: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAeCAYAAABuUU38AAAACXBIWXMAAAsTAAALEwEAmpwYAAABzElEQVRYhe2YQXKCMBSG/2eZju3CqrDvjOg5PII7b9EzaM/QU+hKb6DnUHoCUctCXTi8LpgAQlqBZAbo9Ftp8gj/z0vyILSYrTAaD7Gcrxk1ZDQe0nK+BjEzlvM1ExHanaeydeXieDiDmTEaD4kWsxUDQKNBeGnXy8jX8QzfDyaSAQCd7nPYaZqtclTlxHW98MEf9qfAiECYaLaM9JUV4uJdYZotuK4XtqUUFzFhbyfSdqf/XijuHs2WgYt3vWkzQLnGkDOw0m3OLlvcRhKXFwIMUnBif04AH3KBtgV7k8iALG4QxeXNjIAAGIBC+bAlwuLIhP8Up5iZhtLVFeLfSNWojpFHFSmUriNZCOtB1sWchdeuwu7FxYz8VA9SBZA+oj/ODk7vl34xbsHdS9/UojtD+druJEXjGin3c0afES7XiL7X3IEFezsNfjMDREDfjPp7VrRJEAENAnpdbbc3tD7IuPAkD9C7y8VgrlIdUeTPGCm0Rhx+A5CoA5I6co+b67cuHHsK9IsoqlJGFBer0q4lzUxeRCYLZkKQykjyW7iKyDTSYrbiuh4HCcLjoMP+BCA434oHVB2hGwimlo5zlLIhsdgJAMcd1ggCgG++oJDts2wnOwAAAABJRU5ErkJggg==', - url: 'https://discord.com/invite/n6VVwDFp?utm_source=Discord%20Widget&utm_medium=Connect', + url: 'https://discord.gg/xFsRQsDnuj', }, { name: 'website', diff --git a/src/providers/DownloadFile.ts b/src/providers/DownloadFile.ts index a207c2a..2c19260 100644 --- a/src/providers/DownloadFile.ts +++ b/src/providers/DownloadFile.ts @@ -3,7 +3,11 @@ const fs = require('fs'); import { Ref } from 'vue'; const path = require('path'); -function downloadFile(url: string, savePath: string, ref?: Ref): Promise { +export function downloadFile( + url: string, + savePath: string, + ref?: Ref +): Promise { return new Promise((resolve, reject) => { const name = savePath.split(path.sep).pop(); const file = fs.createWriteStream(savePath); @@ -71,5 +75,3 @@ function downloadFile(url: string, savePath: string, ref?: Ref): Promise { }); }); } - -export { downloadFile }; diff --git a/src/providers/DownloadMods.ts b/src/providers/DownloadMods.ts new file mode 100644 index 0000000..49ed945 --- /dev/null +++ b/src/providers/DownloadMods.ts @@ -0,0 +1,51 @@ +import { storeToRefs } from 'pinia'; +import { useInstallerStore } from 'src/stores/InstallerStore'; +import { useSourcesStore } from 'src/stores/SourcesStore'; +import { useStateStore } from 'src/stores/StateStore'; +import { Ref } from 'vue'; +import { downloadFile } from './DownloadFile'; +const fs = require('fs'); +const path = require('path'); + +export async function downloadMods(ref: Ref) { + const startTime = Date.now(); + + const { launcher } = storeToRefs(useSourcesStore()); + const { minecraftDir } = storeToRefs(useInstallerStore()); + const { processingMods } = storeToRefs(useStateStore()); + + const modList = launcher.value.modList; + const modLoader = launcher.value.fabric.name; + const totalMods = modList.length; + let downloaded = 0; + + const modsPath = path.join(minecraftDir.value, 'mods'); + if (!fs.existsSync(modsPath)) fs.mkdirSync(modsPath, { recursive: true }); + + async function processArray(mods: typeof modList): Promise { + for (const mod of mods) { + const { name, mod_version, mc_version, source } = mod; + const modName = + name + ' - ' + modLoader + mod_version + '-' + mc_version + '.jar'; + console.log(`Downloading ${modName} to ${modsPath}`); + const modPath = path.join(modsPath, modName); + await downloadFile(source, modPath); + downloaded++; + const progress = downloaded / totalMods; + const percent = (progress * 100).toFixed(1); + + ref.value.progress = progress; + ref.value.percent = percent; + // Note: Roughly 21 character limit, consider truncating mod name + ref.value.label = `Downloading: ${name}`; + + const taken = Date.now() - startTime; + console.log(`downloaded mod ${downloaded}/${totalMods} in ${taken}ms`); + } + } + await processArray(modList); + ref.value.label = 'Mods successfully downloaded!'; + ref.value.img = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAeCAYAAABuUU38AAAACXBIWXMAAAsTAAALEwEAmpwYAAACqElEQVRYhdVYzY7SUBT+Tu1CTWRaG4UncKMOxOWMhhg37khYwBgT43P4ADyIiSawMLBzA0LUWRLGv4VPwMwECjVhiJYeF6W1pTBD7+2Mw7e67b0993733HvOd0r1ahuFUh6NWoexgSiU8tSodUDMjEatw0QETb/2v9cVCyPzBMyMQilPVK+2GQAUhbClbRaR8egEjuMeJBUA9JvX/U7DSElP8OJp59T+1+/z0nMMBpa/8eZw4hLx4JG4mlKjX8ZALqOv7Ov1TWn7U8uGYaQwGFj+u4hFkUnKO02/zQBq+09OHb+324LDDFpz/LI1Ti079E4FxbKxFPfSGq6Qa+jn8NeZ47fTmt/u9U35BRCgkgST8m4TzPF39NW7B6HnZw9bmDkc244HAqC4zhVDNq1DIXmX3r+tQVXk7Ciyi0jgZCYCKSIOMxTJnfRAkp4VioOVYhffjsZ4++mx0KSlQJQjANX53agUu+j1TaG7IhzQbccR/TSUZw4OE4haIHEiIijvtsDMyGV0KY9GwRdLJBvIHw4nK7YTIbI332lvaYtnPOiJ78djvPmYlCf+IREiwUz95XAU6c+mNZ/kzDmfsicRIl6mrhS72E5roagEuES/Ho1Q/SyWudeBMBFVWZ2CglGJ4YZYGSm0DqhebbNXjxhGKpb6fZnvYPLHjtyJSrHrtw/6pp8nVqG00wQRYnlsatm+jDeHE7nMfse4kYjWymV0Xz2L4lzC76K6vQjIi8ZLohqlPOLtvFeXeDhLKy1GNdE6JIhEjlY2HdBOa1R8yWutJUSmlh27bl+8E88ffcDM4YgMUYjgsHglGFzjIkIrHgwsGEZq6cA4uHtra+n737MZfhxb0vYBhP6gAPM84j0E/29tAszhxG8ruDzVqgzIO1oEgIMMNwgEAH8Bhif2yStQayoAAAAOZVhJZk1NACoAAAAIAAAAAAAAANJTkwAAAABJRU5ErkJggg=='; + processingMods.value = false; +} diff --git a/src/providers/DownloadResourcePack.ts b/src/providers/DownloadResourcePack.ts new file mode 100644 index 0000000..e8eb078 --- /dev/null +++ b/src/providers/DownloadResourcePack.ts @@ -0,0 +1,33 @@ +import { storeToRefs } from 'pinia'; +import { useInstallerStore } from 'src/stores/InstallerStore'; +import { useSourcesStore } from 'src/stores/SourcesStore'; +import { useStateStore } from 'src/stores/StateStore'; +import { Ref } from 'vue'; +import { downloadFile } from './DownloadFile'; +import { updateMinecraftOpts } from './UpdateMinecraftOpts'; +const path = require('path'); + +export async function downloadResourcePack(ref: Ref) { + const { resourcePack } = storeToRefs(useSourcesStore()); + const { minecraftDir } = storeToRefs(useInstallerStore()); + const { processingResourcepack } = storeToRefs(useStateStore()); + ref.value.label = 'Downloading resource pack'; + + // NOTE: if changing this, edit the value in src/providers/UpdateMinecraftOpts.ts + const rpName = `Drehmal Resource Pack v${resourcePack.value.version}.zip`; + const filePath = path.join(minecraftDir.value, 'resourcepacks', rpName); + console.log(`Downloading resource pack to: ${filePath}`); + downloadFile(resourcePack.value.source, filePath, ref).then(() => { + ref.value.label = 'Updating video settings'; + + const optionsFilePath = path.join(minecraftDir.value, 'options.txt'); + + console.log(`Updating Minecraft options in file: ${optionsFilePath}`); + updateMinecraftOpts(optionsFilePath); + + ref.value.label = 'Resource pack downloaded!'; + + console.log('Updated Resource Pack order and video settings'); + processingResourcepack.value = false; + }); +} diff --git a/src/providers/DownloadShards.ts b/src/providers/DownloadShards.ts index d11ab82..80fbc5e 100644 --- a/src/providers/DownloadShards.ts +++ b/src/providers/DownloadShards.ts @@ -4,7 +4,7 @@ import { ShardsBox } from 'src/components/models'; import { Ref } from 'vue'; const path = require('path'); -function downloadShards( +export function downloadShards( url: string, savePath: string, ref: Ref @@ -74,5 +74,3 @@ function downloadShards( }); }); } - -export { downloadShards }; diff --git a/src/providers/ExtractFile.ts b/src/providers/ExtractFile.ts deleted file mode 100644 index 3938996..0000000 --- a/src/providers/ExtractFile.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Ref } from 'vue'; -const fs = require('fs'); -const tarpack = require('tar-pack'); - -export function extractTargz( - archivePath: string, - outputPath: string, - ref?: Ref -): Promise { - return new Promise((resolve, reject) => { - fs.createReadStream(archivePath).pipe( - tarpack.unpack( - outputPath, - { keepFiles: true, strip: 0 }, - function (err: any) { - if (err) console.error(err.stack); - else { - if (ref) ref.value.label = 'Files successfully extracted!'; - - console.log(`Extraction complete. Removing ${archivePath}`); - fs.unlinkSync(archivePath); - resolve(); - } - } - ) - ); - }); -} diff --git a/src/providers/InstallFabric.ts b/src/providers/InstallFabric.ts index e0aa21c..e64b722 100644 --- a/src/providers/InstallFabric.ts +++ b/src/providers/InstallFabric.ts @@ -92,75 +92,3 @@ export async function installFabric(ref: Ref) { }); }); } -export async function downloadMods(ref: Ref) { - const startTime = Date.now(); - - const { launcher } = storeToRefs(useSourcesStore()); - const { minecraftDir } = storeToRefs(useInstallerStore()); - const { processingMods } = storeToRefs(useStateStore()); - - const modList = launcher.value.modList; - const modLoader = launcher.value.fabric.name; - const totalMods = modList.length; - let downloaded = 0; - - const modsPath = path.join(minecraftDir.value, 'mods'); - if (!fs.existsSync(modsPath)) fs.mkdirSync(modsPath, { recursive: true }); - - async function processArray(mods: typeof modList): Promise { - for (const mod of mods) { - const { name, mod_version, mc_version, source } = mod; - const modName = - name + ' - ' + modLoader + mod_version + '-' + mc_version + '.jar'; - console.log(`Downloading ${modName} to ${modsPath}`); - const modPath = path.join(modsPath, modName); - await downloadFile(source, modPath); - downloaded++; - const progress = downloaded / totalMods; - const percent = (progress * 100).toFixed(1); - - ref.value.progress = progress; - ref.value.percent = percent; - // Note: Roughly 21 character limit, consider truncating mod name - ref.value.label = `Downloading: ${name}`; - - const taken = Date.now() - startTime; - console.log(`downloaded mod ${downloaded}/${totalMods} in ${taken}ms`); - } - } - await processArray(modList); - ref.value.label = 'Mods successfully downloaded!'; - ref.value.img = - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAeCAYAAABuUU38AAAACXBIWXMAAAsTAAALEwEAmpwYAAACqElEQVRYhdVYzY7SUBT+Tu1CTWRaG4UncKMOxOWMhhg37khYwBgT43P4ADyIiSawMLBzA0LUWRLGv4VPwMwECjVhiJYeF6W1pTBD7+2Mw7e67b0993733HvOd0r1ahuFUh6NWoexgSiU8tSodUDMjEatw0QETb/2v9cVCyPzBMyMQilPVK+2GQAUhbClbRaR8egEjuMeJBUA9JvX/U7DSElP8OJp59T+1+/z0nMMBpa/8eZw4hLx4JG4mlKjX8ZALqOv7Ov1TWn7U8uGYaQwGFj+u4hFkUnKO02/zQBq+09OHb+324LDDFpz/LI1Ti079E4FxbKxFPfSGq6Qa+jn8NeZ47fTmt/u9U35BRCgkgST8m4TzPF39NW7B6HnZw9bmDkc244HAqC4zhVDNq1DIXmX3r+tQVXk7Ciyi0jgZCYCKSIOMxTJnfRAkp4VioOVYhffjsZ4++mx0KSlQJQjANX53agUu+j1TaG7IhzQbccR/TSUZw4OE4haIHEiIijvtsDMyGV0KY9GwRdLJBvIHw4nK7YTIbI332lvaYtnPOiJ78djvPmYlCf+IREiwUz95XAU6c+mNZ/kzDmfsicRIl6mrhS72E5roagEuES/Ho1Q/SyWudeBMBFVWZ2CglGJ4YZYGSm0DqhebbNXjxhGKpb6fZnvYPLHjtyJSrHrtw/6pp8nVqG00wQRYnlsatm+jDeHE7nMfse4kYjWymV0Xz2L4lzC76K6vQjIi8ZLohqlPOLtvFeXeDhLKy1GNdE6JIhEjlY2HdBOa1R8yWutJUSmlh27bl+8E88ffcDM4YgMUYjgsHglGFzjIkIrHgwsGEZq6cA4uHtra+n737MZfhxb0vYBhP6gAPM84j0E/29tAszhxG8ruDzVqgzIO1oEgIMMNwgEAH8Bhif2yStQayoAAAAOZVhJZk1NACoAAAAIAAAAAAAAANJTkwAAAABJRU5ErkJggg=='; - processingMods.value = false; -} - -export async function downloadResourcePack(ref: Ref) { - const { resourcePack } = storeToRefs(useSourcesStore()); - const { minecraftDir } = storeToRefs(useInstallerStore()); - const { processingResourcepack } = storeToRefs(useStateStore()); - ref.value.label = 'Downloading resource pack'; - - const rpName = `Drehmal Resource Pack v${resourcePack.value.version}.zip`; - const filePath = path.join(minecraftDir.value, 'resourcepacks', rpName); - console.log(`Downloading resource pack to: ${filePath}`); - downloadFile(resourcePack.value.source, filePath, ref).then(() => { - ref.value.label = 'Updating resource pack order'; - - const optionsFilePath = path.join(minecraftDir.value, 'options.txt'); - const data = fs.readFileSync(optionsFilePath).toString().split('\n'); - const resourceOptIndex = data.findIndex( - (item: string[]) => item.indexOf('resourcePacks:') !== -1 - ); - data[ - resourceOptIndex - ] = `resourcePacks:["vanilla","Fabric Mods","file/${rpName}"]`; - - console.log(`Updating resource pack order in file: ${optionsFilePath}`); - fs.writeFileSync(optionsFilePath, data.join('\n'), 'utf-8'); - ref.value.label = 'Resource pack downloaded!'; - - console.log('Updated Resource Pack order'); - processingResourcepack.value = false; - }); -} diff --git a/src/providers/UpdateMinecraftOpts.ts b/src/providers/UpdateMinecraftOpts.ts new file mode 100644 index 0000000..97b1cec --- /dev/null +++ b/src/providers/UpdateMinecraftOpts.ts @@ -0,0 +1,41 @@ +import { storeToRefs } from 'pinia'; +import { useSourcesStore } from 'src/stores/SourcesStore'; + +const fs = require('fs'); + +export function updateMinecraftOpts(filename: string) { + // Parse options file into an array of strings for each line + const lines: string[] = fs.readFileSync(filename, 'utf-8').split('\n'); + // Create object to store the options as a json object for easy access + const options: Record = {}; + // add each option to the created object + lines.forEach((line) => { + const [key, value] = line.split(':'); + if (key && value) { + options[key.trim()] = value.trim(); + } + }); + + const { resourcePack } = storeToRefs(useSourcesStore()); + const rpName = `Drehmal Resource Pack v${resourcePack.value.version}.zip`; + + // modify options here + // update resource pack order to include Drehmal's pack at the top + options['resourcePacks'] = `["vanilla","Fabric Mods","file/${rpName}"]`; + // disable clouds, gets in the way of towers/structures + options['renderClouds'] = 'false'; + // increase render distance to required minimum (consider scale based on memory allocation) + if (parseInt(options['renderDistance']) < 8) options['renderDistance'] = '8'; + // maximise unfocused chat size so players can properly use menus + options['chatHeightUnfocused'] = '1.0'; + // ensure custom music is enabled + options['soundCategory_music'] = '1.0'; + options['soundCategory_record'] = '1.0'; + + let newOptions = ''; + Object.keys(options).forEach( + (key) => (newOptions += `${key}:${options[key]}\n`) + ); + + fs.writeFileSync(filename, newOptions); +}