Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

steamworks: packager part #880

Merged
merged 2 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
32 changes: 32 additions & 0 deletions src/p4/PackagerOptions.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -1009,6 +1011,36 @@
</div>
{/if}

{#if projectData.project.analysis.usesSteamworks}
<Section
accent="#136C9F"
reset={() => {
resetOptions([
'steamworks'
]);
}}
>
<h2>Steamworks</h2>
{#if $options.target.startsWith('electron-')}
<p>{$_('options.steamworksAvailable')}</p>
<label class="option">
{$_('options.steamworksAppId')}
<input pattern="\d+" minlength="1" bind:value={$options.steamworks.appId}>
</label>
<label class="option">
{$_('options.steamworksOnError')}
<select bind:value={$options.steamworks.onError}>
<option value="ignore">{$_('options.steamworksIgnore')}</option>
<option value="warning">{$_('options.steamworksWarning')}</option>
<option value="error">{$_('options.steamworksError')}</option>
</select>
</label>
{:else}
<p>{$_('options.steamworksUnavailable')}</p>
{/if}
</Section>
{/if}

<Section>
<DropArea on:drop={(e) => importOptionsFromDataTransfer(e.detail)}>
<div class="buttons">
Expand Down
3 changes: 3 additions & 0 deletions src/packager/download-project.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const unknownAnalysis = () => ({
stageVariables: [],
stageComments: [],
usesMusic: true,
usesSteamworks: false,
extensions: []
});

Expand Down Expand Up @@ -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
};
};
Expand Down
5 changes: 5 additions & 0 deletions src/packager/large-assets.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
111 changes: 109 additions & 2 deletions src/packager/packager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down