Skip to content

Commit 400ee8e

Browse files
committed
Support for GOG
1 parent 81e3d2b commit 400ee8e

File tree

12 files changed

+1008
-5
lines changed

12 files changed

+1008
-5
lines changed

electron_gog/favicon.icns

48.5 KB
Binary file not shown.

electron_gog/favicon.ico

105 KB
Binary file not shown.

electron_gog/favicon.png

21 KB
Loading

electron_gog/index.js

Lines changed: 381 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
1+
/* eslint-disable quotes,no-undef */
2+
3+
const { app, BrowserWindow, Menu, MenuItem, ipcMain, shell, dialog, session } = require("electron");
4+
const path = require("path");
5+
const url = require("url");
6+
const fs = require("fs");
7+
const asyncLock = require("async-lock");
8+
const windowStateKeeper = require("electron-window-state");
9+
10+
// Disable hardware key handling, i.e. being able to pause/resume the game music
11+
// with hardware keys
12+
app.commandLine.appendSwitch("disable-features", "HardwareMediaKeyHandling");
13+
14+
const isDev = app.commandLine.hasSwitch("dev");
15+
const isLocal = app.commandLine.hasSwitch("local");
16+
const safeMode = app.commandLine.hasSwitch("safe-mode");
17+
const externalMod = app.commandLine.getSwitchValue("load-mod");
18+
19+
const roamingFolder =
20+
process.env.APPDATA ||
21+
(process.platform == "darwin"
22+
? process.env.HOME + "/Library/Preferences"
23+
: process.env.HOME + "/.local/share");
24+
25+
let storePath = path.join(roamingFolder, "shapez.io", "saves");
26+
let modsPath = path.join(roamingFolder, "shapez.io", "mods");
27+
28+
if (!fs.existsSync(storePath)) {
29+
// No try-catch by design
30+
fs.mkdirSync(storePath, { recursive: true });
31+
}
32+
33+
if (!fs.existsSync(modsPath)) {
34+
fs.mkdirSync(modsPath, { recursive: true });
35+
}
36+
37+
/** @type {BrowserWindow} */
38+
let win = null;
39+
let menu = null;
40+
41+
function createWindow() {
42+
let faviconExtension = ".png";
43+
if (process.platform === "win32") {
44+
faviconExtension = ".ico";
45+
}
46+
47+
const mainWindowState = windowStateKeeper({
48+
defaultWidth: 1000,
49+
defaultHeight: 800,
50+
});
51+
52+
win = new BrowserWindow({
53+
x: mainWindowState.x,
54+
y: mainWindowState.y,
55+
width: mainWindowState.width,
56+
height: mainWindowState.height,
57+
show: false,
58+
backgroundColor: "#222428",
59+
useContentSize: false,
60+
minWidth: 800,
61+
minHeight: 600,
62+
title: "shapez",
63+
transparent: false,
64+
icon: path.join(__dirname, "favicon" + faviconExtension),
65+
// fullscreen: true,
66+
autoHideMenuBar: !isDev,
67+
webPreferences: {
68+
nodeIntegration: false,
69+
nodeIntegrationInWorker: false,
70+
nodeIntegrationInSubFrames: false,
71+
contextIsolation: true,
72+
enableRemoteModule: false,
73+
disableBlinkFeatures: "Auxclick",
74+
75+
webSecurity: true,
76+
sandbox: true,
77+
preload: path.join(__dirname, "preload.js"),
78+
experimentalFeatures: false,
79+
},
80+
allowRunningInsecureContent: false,
81+
});
82+
83+
mainWindowState.manage(win);
84+
85+
if (isLocal) {
86+
win.loadURL("http://localhost:3005");
87+
} else {
88+
win.loadURL(
89+
url.format({
90+
pathname: path.join(__dirname, "index.html"),
91+
protocol: "file:",
92+
slashes: true,
93+
})
94+
);
95+
}
96+
win.webContents.session.clearCache();
97+
win.webContents.session.clearStorageData();
98+
99+
////// SECURITY
100+
101+
// Disable permission requests
102+
win.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => {
103+
callback(false);
104+
});
105+
session.fromPartition("default").setPermissionRequestHandler((webContents, permission, callback) => {
106+
callback(false);
107+
});
108+
109+
app.on("web-contents-created", (event, contents) => {
110+
// Disable vewbiew
111+
contents.on("will-attach-webview", (event, webPreferences, params) => {
112+
event.preventDefault();
113+
});
114+
// Disable navigation
115+
contents.on("will-navigate", (event, navigationUrl) => {
116+
event.preventDefault();
117+
});
118+
});
119+
120+
win.webContents.on("will-redirect", (contentsEvent, navigationUrl) => {
121+
// Log and prevent the app from redirecting to a new page
122+
console.error(
123+
`The application tried to redirect to the following address: '${navigationUrl}'. This attempt was blocked.`
124+
);
125+
contentsEvent.preventDefault();
126+
});
127+
128+
// Filter loading any module via remote;
129+
// you shouldn't be using remote at all, though
130+
// https://electronjs.org/docs/tutorial/security#16-filter-the-remote-module
131+
app.on("remote-require", (event, webContents, moduleName) => {
132+
event.preventDefault();
133+
});
134+
135+
// built-ins are modules such as "app"
136+
app.on("remote-get-builtin", (event, webContents, moduleName) => {
137+
event.preventDefault();
138+
});
139+
140+
app.on("remote-get-global", (event, webContents, globalName) => {
141+
event.preventDefault();
142+
});
143+
144+
app.on("remote-get-current-window", (event, webContents) => {
145+
event.preventDefault();
146+
});
147+
148+
app.on("remote-get-current-web-contents", (event, webContents) => {
149+
event.preventDefault();
150+
});
151+
152+
//// END SECURITY
153+
154+
win.webContents.on("new-window", (event, pth) => {
155+
event.preventDefault();
156+
157+
if (pth.startsWith("https://")) {
158+
shell.openExternal(pth);
159+
}
160+
});
161+
162+
win.on("closed", () => {
163+
console.log("Window closed");
164+
win = null;
165+
});
166+
167+
if (isDev) {
168+
menu = new Menu();
169+
170+
win.webContents.toggleDevTools();
171+
172+
const mainItem = new MenuItem({
173+
label: "Toggle Dev Tools",
174+
click: () => win.webContents.toggleDevTools(),
175+
accelerator: "F12",
176+
});
177+
menu.append(mainItem);
178+
179+
const reloadItem = new MenuItem({
180+
label: "Reload",
181+
click: () => win.reload(),
182+
accelerator: "F5",
183+
});
184+
menu.append(reloadItem);
185+
186+
const fullscreenItem = new MenuItem({
187+
label: "Fullscreen",
188+
click: () => win.setFullScreen(!win.isFullScreen()),
189+
accelerator: "F11",
190+
});
191+
menu.append(fullscreenItem);
192+
193+
const mainMenu = new Menu();
194+
mainMenu.append(
195+
new MenuItem({
196+
label: "shapez.io",
197+
submenu: menu,
198+
})
199+
);
200+
201+
Menu.setApplicationMenu(mainMenu);
202+
} else {
203+
Menu.setApplicationMenu(null);
204+
}
205+
206+
win.once("ready-to-show", () => {
207+
win.show();
208+
win.focus();
209+
});
210+
}
211+
212+
if (!app.requestSingleInstanceLock()) {
213+
app.exit(0);
214+
} else {
215+
app.on("second-instance", () => {
216+
// Someone tried to run a second instance, we should focus
217+
if (win) {
218+
if (win.isMinimized()) {
219+
win.restore();
220+
}
221+
win.focus();
222+
}
223+
});
224+
}
225+
226+
app.on("ready", createWindow);
227+
228+
app.on("window-all-closed", () => {
229+
console.log("All windows closed");
230+
app.quit();
231+
});
232+
233+
ipcMain.on("set-fullscreen", (event, flag) => {
234+
win.setFullScreen(flag);
235+
});
236+
237+
ipcMain.on("exit-app", () => {
238+
win.close();
239+
app.quit();
240+
});
241+
242+
let renameCounter = 1;
243+
244+
const fileLock = new asyncLock({
245+
timeout: 30000,
246+
maxPending: 1000,
247+
});
248+
249+
function niceFileName(filename) {
250+
return filename.replace(storePath, "@");
251+
}
252+
253+
async function writeFileSafe(filename, contents) {
254+
++renameCounter;
255+
const prefix = "[ " + renameCounter + ":" + niceFileName(filename) + " ] ";
256+
const transactionId = String(new Date().getTime()) + "." + renameCounter;
257+
258+
if (fileLock.isBusy()) {
259+
console.warn(prefix, "Concurrent write process on", filename);
260+
}
261+
262+
fileLock.acquire(filename, async () => {
263+
console.log(prefix, "Starting write on", niceFileName(filename), "in transaction", transactionId);
264+
265+
if (!fs.existsSync(filename)) {
266+
// this one is easy
267+
console.log(prefix, "Writing file instantly because it does not exist:", niceFileName(filename));
268+
await fs.promises.writeFile(filename, contents, "utf8");
269+
return;
270+
}
271+
272+
// first, write a temporary file (.tmp-XXX)
273+
const tempName = filename + ".tmp-" + transactionId;
274+
console.log(prefix, "Writing temporary file", niceFileName(tempName));
275+
await fs.promises.writeFile(tempName, contents, "utf8");
276+
277+
// now, rename the original file to (.backup-XXX)
278+
const oldTemporaryName = filename + ".backup-" + transactionId;
279+
console.log(
280+
prefix,
281+
"Renaming old file",
282+
niceFileName(filename),
283+
"to",
284+
niceFileName(oldTemporaryName)
285+
);
286+
await fs.promises.rename(filename, oldTemporaryName);
287+
288+
// now, rename the temporary file (.tmp-XXX) to the target
289+
console.log(
290+
prefix,
291+
"Renaming the temporary file",
292+
niceFileName(tempName),
293+
"to the original",
294+
niceFileName(filename)
295+
);
296+
await fs.promises.rename(tempName, filename);
297+
298+
// we are done now, try to create a backup, but don't fail if the backup fails
299+
try {
300+
// check if there is an old backup file
301+
const backupFileName = filename + ".backup";
302+
if (fs.existsSync(backupFileName)) {
303+
console.log(prefix, "Deleting old backup file", niceFileName(backupFileName));
304+
// delete the old backup
305+
await fs.promises.unlink(backupFileName);
306+
}
307+
308+
// rename the old file to the new backup file
309+
console.log(prefix, "Moving", niceFileName(oldTemporaryName), "to the backup file location");
310+
await fs.promises.rename(oldTemporaryName, backupFileName);
311+
} catch (ex) {
312+
console.error(prefix, "Failed to switch backup files:", ex);
313+
}
314+
});
315+
}
316+
317+
ipcMain.handle("fs-job", async (event, job) => {
318+
const filenameSafe = job.filename.replace(/[^a-z\.\-_0-9]/gi, "_");
319+
const fname = path.join(storePath, filenameSafe);
320+
switch (job.type) {
321+
case "read": {
322+
if (!fs.existsSync(fname)) {
323+
// Special FILE_NOT_FOUND error code
324+
return { error: "file_not_found" };
325+
}
326+
return await fs.promises.readFile(fname, "utf8");
327+
}
328+
case "write": {
329+
await writeFileSafe(fname, job.contents);
330+
return job.contents;
331+
}
332+
333+
case "delete": {
334+
await fs.promises.unlink(fname);
335+
return;
336+
}
337+
338+
default:
339+
throw new Error("Unknown fs job: " + job.type);
340+
}
341+
});
342+
343+
ipcMain.handle("open-mods-folder", async () => {
344+
shell.openPath(modsPath);
345+
});
346+
347+
console.log("Loading mods ...");
348+
349+
function loadMods() {
350+
if (safeMode) {
351+
console.log("Safe Mode enabled for mods, skipping mod search");
352+
}
353+
console.log("Loading mods from", modsPath);
354+
let modFiles = safeMode
355+
? []
356+
: fs
357+
.readdirSync(modsPath)
358+
.filter(filename => filename.endsWith(".js"))
359+
.map(filename => path.join(modsPath, filename));
360+
361+
if (externalMod) {
362+
console.log("Adding external mod source:", externalMod);
363+
const externalModPaths = externalMod.split(",");
364+
modFiles = modFiles.concat(externalModPaths);
365+
}
366+
367+
return modFiles.map(filename => fs.readFileSync(filename, "utf8"));
368+
}
369+
370+
let mods = [];
371+
try {
372+
mods = loadMods();
373+
console.log("Loaded", mods.length, "mods");
374+
} catch (ex) {
375+
console.error("Failed to load mods");
376+
dialog.showErrorBox("Failed to load mods:", ex);
377+
}
378+
379+
ipcMain.handle("get-mods", async () => {
380+
return mods;
381+
});

electron_gog/package.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "electron",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"license": "MIT",
6+
"private": true,
7+
"scripts": {
8+
"startDev": "electron --disable-direct-composition --in-process-gpu . --dev --local",
9+
"startDevGpu": "electron --enable-gpu-rasterization --enable-accelerated-2d-canvas --num-raster-threads=8 --enable-zero-copy . --dev --local",
10+
"start": "electron --disable-direct-composition --in-process-gpu ."
11+
},
12+
"dependencies": {
13+
"async-lock": "^1.2.8",
14+
"electron": "16.2.8",
15+
"electron-window-state": "^5.0.3"
16+
}
17+
}

0 commit comments

Comments
 (0)