diff --git a/src/locales/en.json b/src/locales/en.json index 844ebbdf..0b2a4b98 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -557,6 +557,27 @@ "maxTextureDimension": { "string": "Increase max vector costume resolution to make large costumes look better. May increase memory use and cause crashes." }, + "steamworksUnavailable": { + "string": "To enable the Steamworks extension, you must use one of the Electron environments." + }, + "steamworksAvailable": { + "string": "This project is using the Steamworks extension. You can find your game's app ID in Steamworks. 480 is the ID of the Steamworks demo game (Spacewar)." + }, + "steamworksAppId": { + "string": "App ID" + }, + "steamworksOnError": { + "string": "If there is an error initializing Steamworks" + }, + "steamworksIgnore": { + "string": "Do nothing" + }, + "steamworksWarning": { + "string": "Show a warning but continue" + }, + "steamworksError": { + "string": "Show an error and exit" + }, "package": { "string": "Package", "context": "Button to package the project" diff --git a/src/p4/PackagerOptions.svelte b/src/p4/PackagerOptions.svelte index 181b7fc5..dc8e0fdc 100644 --- a/src/p4/PackagerOptions.svelte +++ b/src/p4/PackagerOptions.svelte @@ -151,6 +151,8 @@ thing = 'Electron'; } else if (detail.asset === 'webview-mac') { thing = 'WKWebView'; + } else if (detail.asset === 'steamworks.js') { + thing = 'Steamworks.js'; } if (thing) { task.setProgressText($_('progress.loadingLargeAsset').replace('{thing}', thing)); @@ -1009,6 +1011,36 @@ {/if} +{#if projectData.project.analysis.usesSteamworks} +
{ + resetOptions([ + 'steamworks' + ]); + }} + > +

Steamworks

+ {#if $options.target.startsWith('electron-')} +

{$_('options.steamworksAvailable')}

+ + + {:else} +

{$_('options.steamworksUnavailable')}

+ {/if} +
+{/if} +
importOptionsFromDataTransfer(e.detail)}>
diff --git a/src/packager/download-project.js b/src/packager/download-project.js index 9e83ad3d..c75cbf51 100644 --- a/src/packager/download-project.js +++ b/src/packager/download-project.js @@ -5,6 +5,7 @@ const unknownAnalysis = () => ({ stageVariables: [], stageComments: [], usesMusic: true, + usesSteamworks: false, extensions: [] }); @@ -40,12 +41,14 @@ const analyzeScratch3 = (projectData) => { .map((i) => i.text); // TODO: usesMusic has possible false negatives const usesMusic = projectData.extensions.includes('music'); + const usesSteamworks = projectData.extensions.includes('steamworks'); const extensions = projectData.extensionURLs ? Object.values(projectData.extensionURLs) : []; return { ...unknownAnalysis(), stageVariables, stageComments, usesMusic, + usesSteamworks, extensions }; }; diff --git a/src/packager/large-assets.js b/src/packager/large-assets.js index 129811db..f9bf51fe 100644 --- a/src/packager/large-assets.js +++ b/src/packager/large-assets.js @@ -81,6 +81,11 @@ export default { sha256: 'b5636571cd9be2aae2f6dac1ab090fdf829c8fdfe91f462cc2feb2d324705f9f', estimatedSize: 3425601 }, + 'steamworks.js': { + src: externalFile('steamworks.js-0.3.2.zip'), + sha256: 'fd8bc80a97cd880d71113dfc5f81b124b6e212335393db73e3df168c5c546fbc', + estimatedSize: 3279554, + }, scaffolding: { src: relativeScaffolding('scaffolding-full.js'), estimatedSize: 4564032, diff --git a/src/packager/packager.js b/src/packager/packager.js index 0ea2d982..0706c292 100644 --- a/src/packager/packager.js +++ b/src/packager/packager.js @@ -525,6 +525,7 @@ cd "$(dirname "$0")" const contentsPrefix = isMac ? `${rootPrefix}${packageName}.app/Contents/` : rootPrefix; const resourcesPrefix = isMac ? `${contentsPrefix}Resources/app/` : `${contentsPrefix}resources/app/`; const electronMainName = 'electron-main.js'; + const electronPreloadName = 'electron-preload.js'; const iconName = 'icon.png'; const icon = await Adapter.getAppIcon(this.options.app.icon); @@ -537,8 +538,8 @@ cd "$(dirname "$0")" }; zip.file(`${resourcesPrefix}package.json`, JSON.stringify(manifest, null, 4)); - const mainJS = `'use strict'; -const {app, BrowserWindow, Menu, shell, screen, dialog} = require('electron'); + let mainJS = `'use strict'; +const {app, BrowserWindow, Menu, shell, screen, dialog, ipcMain} = require('electron'); const path = require('path'); const isWindows = process.platform === 'win32'; @@ -571,6 +572,7 @@ const createWindow = (windowOptions) => { sandbox: true, contextIsolation: true, nodeIntegration: false, + preload: path.resolve(__dirname, ${JSON.stringify(electronPreloadName)}), }, frame: ${this.options.app.windowControls !== 'frameless'}, show: true, @@ -725,7 +727,106 @@ app.whenReady().then(() => { createProjectWindow(defaultProjectURL); }); `; + + let preloadJS = `'use strict'; +const {contextBridge, ipcRenderer} = require('electron'); +`; + + if (this.project.analysis.usesSteamworks) { + mainJS += ` + const enableSteamworks = () => { + const APP_ID = +${JSON.stringify(this.options.steamworks.appId)}; + const steamworks = require('./steamworks.js/'); + + const client = steamworks.init(APP_ID); + + const async = (event, callback) => ipcMain.handle(event, (e, ...args) => { + return callback(...args); + }); + const sync = (event, callback) => ipcMain.on(event, (e, ...args) => { + e.returnValue = callback(...args); + }); + + async('Steamworks.achievement.activate', (achievement) => client.achievement.activate(achievement)); + async('Steamworks.achievement.clear', (achievement) => client.achievement.clear(achievement)); + sync('Steamworks.achievement.isActivated', (achievement) => client.achievement.isActivated(achievement)); + sync('Steamworks.apps.isDlcInstalled', (dlc) => client.apps.isDlcInstalled(dlc)); + sync('Steamworks.localplayer.getName', () => client.localplayer.getName()); + sync('Steamworks.localplayer.getLevel', () => client.localplayer.getLevel()); + sync('Steamworks.localplayer.getIpCountry', () => client.localplayer.getIpCountry()); + sync('Steamworks.localplayer.getSteamId', () => client.localplayer.getSteamId()); + async('Steamworks.overlay.activateToWebPage', (url) => client.overlay.activateToWebPage(url)); + + steamworks.electronEnableSteamOverlay(); + sync('Steamworks.ok', () => true); + }; + + try { + enableSteamworks(); + } catch (e) { + console.error(e); + ipcMain.on('Steamworks.ok', (e) => { + e.returnValue = false; + }); + app.whenReady().then(() => { + const ON_ERROR = ${JSON.stringify(this.options.steamworks.onError)}; + const window = BrowserWindow.getAllWindows()[0]; + if (ON_ERROR === 'warning') { + dialog.showMessageBox(window, { + type: 'error', + message: 'Error initializing Steamworks: ' + e, + }); + } else if (ON_ERROR === 'error') { + dialog.showMessageBoxSync(window, { + type: 'error', + message: 'Error initializing Steamworks: ' + e, + }); + app.quit(); + } + }); + }`; + + preloadJS += ` + const enableSteamworks = () => { + const sync = (event) => (...args) => ipcRenderer.sendSync(event, ...args); + const async = (event) => (...args) => ipcRenderer.invoke(event, ...args); + + contextBridge.exposeInMainWorld('Steamworks', { + ok: sync('Steamworks.ok'), + achievement: { + activate: async('Steamworks.achievement.activate'), + clear: async('Steamworks.achievement.clear'), + isActivated: sync('Steamworks.achievement.isActivated'), + }, + apps: { + isDlcInstalled: async('Steamworks.apps.isDlcInstalled'), + }, + leaderboard: { + uploadScore: async('Steamworks.leaderboard.uploadScore'), + }, + localplayer: { + getName: sync('Steamworks.localplayer.getName'), + getLevel: sync('Steamworks.localplayer.getLevel'), + getIpCountry: sync('Steamworks.localplayer.getIpCountry'), + getSteamId: sync('Steamworks.localplayer.getSteamId'), + }, + overlay: { + activateToWebPage: async('Steamworks.overlay.activateToWebPage'), + }, + }); + }; + enableSteamworks();`; + + const steamworksBuffer = await this.fetchLargeAsset('steamworks.js', 'arraybuffer'); + const steamworksZip = await (await getJSZip()).loadAsync(steamworksBuffer); + for (const [path, file] of Object.entries(steamworksZip.files)) { + const newPath = path.replace(/^package\//, 'steamworks.js/'); + setFileFast(zip, `${resourcesPrefix}${newPath}`, file); + } + } + zip.file(`${resourcesPrefix}${electronMainName}`, mainJS); + zip.file(`${resourcesPrefix}${electronPreloadName}`, preloadJS); for (const [path, data] of Object.entries(projectZip.files)) { setFileFast(zip, `${resourcesPrefix}${path}`, data); @@ -1702,6 +1803,12 @@ Packager.DEFAULT_OPTIONS = () => ({ y: 0 } }, + steamworks: { + // 480 is Spacewar, the Steamworks demo game + appId: '480', + // 'ignore' (no alert), 'warning' (alert and continue), or 'error' (alert and exit) + onError: 'warning' + }, extensions: [], bakeExtensions: true, maxTextureDimension: 2048