mirror of
https://github.com/tobspr/shapez.io.git
synced 2025-12-13 02:01:51 +00:00
Merge pull request #32 from cody-ferguson/feat/remove-all-web-stuff
Remove all web related stuff
This commit is contained in:
commit
e142c1211f
@ -78,7 +78,7 @@ and does not intend to provide compatibility for older clients.
|
|||||||
- In the root folder, run `yarn package-$PLATFORM-$ARCH` where:
|
- In the root folder, run `yarn package-$PLATFORM-$ARCH` where:
|
||||||
- `$PLATFORM` is `win32`, `linux` or `darwin` depending on your system.
|
- `$PLATFORM` is `win32`, `linux` or `darwin` depending on your system.
|
||||||
- `$ARCH` is the target system architecture (`x64` or `arm64`)
|
- `$ARCH` is the target system architecture (`x64` or `arm64`)
|
||||||
- The build will be found under `build_output/standalone-steam` as `shapez-...`.
|
- The build will be found under `build_output/standalone` as `shapez-...`.
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,6 @@ const { app, BrowserWindow, Menu, MenuItem, ipcMain, shell, dialog, session } =
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const url = require("url");
|
const url = require("url");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const steam = require("./steam");
|
|
||||||
const asyncLock = require("async-lock");
|
const asyncLock = require("async-lock");
|
||||||
const windowStateKeeper = require("electron-window-state");
|
const windowStateKeeper = require("electron-window-state");
|
||||||
|
|
||||||
@ -153,7 +152,7 @@ function createWindow() {
|
|||||||
win.webContents.on("new-window", (event, pth) => {
|
win.webContents.on("new-window", (event, pth) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (pth.startsWith("https://") || pth.startsWith("steam://")) {
|
if (pth.startsWith("https://")) {
|
||||||
shell.openExternal(pth);
|
shell.openExternal(pth);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -378,10 +377,3 @@ try {
|
|||||||
ipcMain.handle("get-mods", async () => {
|
ipcMain.handle("get-mods", async () => {
|
||||||
return mods;
|
return mods;
|
||||||
});
|
});
|
||||||
|
|
||||||
steam.init(isDev);
|
|
||||||
|
|
||||||
// Only allow achievements and puzzle DLC if no mods are loaded
|
|
||||||
if (mods.length === 0) {
|
|
||||||
steam.listen();
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,112 +0,0 @@
|
|||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
const { ipcMain } = require("electron");
|
|
||||||
|
|
||||||
let greenworks = null;
|
|
||||||
let appId = null;
|
|
||||||
let initialized = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
greenworks = require("shapez.io-private-artifacts/steam/greenworks");
|
|
||||||
appId = parseInt(fs.readFileSync(path.join(__dirname, "steam_appid.txt"), "utf8"));
|
|
||||||
} catch (err) {
|
|
||||||
// greenworks is not installed
|
|
||||||
console.warn("Failed to load steam api:", err);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("App ID:", appId);
|
|
||||||
|
|
||||||
function init(isDev) {
|
|
||||||
if (!greenworks) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isDev) {
|
|
||||||
if (greenworks.restartAppIfNecessary(appId)) {
|
|
||||||
console.log("Restarting ...");
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!greenworks.init()) {
|
|
||||||
console.log("Failed to initialize greenworks");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
initialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function listen() {
|
|
||||||
ipcMain.handle("steam:is-initialized", isInitialized);
|
|
||||||
|
|
||||||
if (!initialized) {
|
|
||||||
console.warn("Steam not initialized, won't be able to listen");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!greenworks) {
|
|
||||||
console.warn("Greenworks not loaded, won't be able to listen");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Adding listeners");
|
|
||||||
|
|
||||||
ipcMain.handle("steam:get-achievement-names", getAchievementNames);
|
|
||||||
ipcMain.handle("steam:activate-achievement", activateAchievement);
|
|
||||||
|
|
||||||
function bufferToHex(buffer) {
|
|
||||||
return Array.from(new Uint8Array(buffer))
|
|
||||||
.map(b => b.toString(16).padStart(2, "0"))
|
|
||||||
.join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcMain.handle("steam:get-ticket", (event, arg) => {
|
|
||||||
console.log("Requested steam ticket ...");
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
greenworks.getAuthSessionTicket(
|
|
||||||
success => {
|
|
||||||
const ticketHex = bufferToHex(success.ticket);
|
|
||||||
resolve(ticketHex);
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
console.error("Failed to get steam ticket:", error);
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle("steam:check-app-ownership", (event, appId) => {
|
|
||||||
return Promise.resolve(greenworks.isDLCInstalled(appId));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function isInitialized(event) {
|
|
||||||
return Promise.resolve(initialized);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAchievementNames(event) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
try {
|
|
||||||
const achievements = greenworks.getAchievementNames();
|
|
||||||
resolve(achievements);
|
|
||||||
} catch (err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function activateAchievement(event, id) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
greenworks.activateAchievement(
|
|
||||||
id,
|
|
||||||
() => resolve(),
|
|
||||||
err => reject(err)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
init,
|
|
||||||
listen,
|
|
||||||
};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
1318690
|
|
||||||
@ -19,7 +19,7 @@ export const BUILD_VARIANTS = {
|
|||||||
standalone: false,
|
standalone: false,
|
||||||
environment: "prod",
|
environment: "prod",
|
||||||
},
|
},
|
||||||
"standalone-steam": {
|
"standalone": {
|
||||||
standalone: true,
|
standalone: true,
|
||||||
executableName: "shapez",
|
executableName: "shapez",
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import packager from "electron-packager";
|
import packager from "electron-packager";
|
||||||
import pj from "../electron/package.json" assert { type: "json" };
|
import pj from "../electron/package.json" with { type: "json" };
|
||||||
import path from "path/posix";
|
import path from "path/posix";
|
||||||
import { getVersion } from "./buildutils.js";
|
import { getVersion } from "./buildutils.js";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
|
|||||||
@ -286,4 +286,4 @@ export const main = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Default task (dev, localhost)
|
// Default task (dev, localhost)
|
||||||
export default gulp.series(serve["standalone-steam"]);
|
export default gulp.series(serve["standalone"]);
|
||||||
|
|||||||
@ -17,8 +17,6 @@ const globalDefs = {
|
|||||||
G_ALL_UI_IMAGES: JSON.stringify(getAllResourceImages()),
|
G_ALL_UI_IMAGES: JSON.stringify(getAllResourceImages()),
|
||||||
|
|
||||||
G_IS_RELEASE: "false",
|
G_IS_RELEASE: "false",
|
||||||
G_IS_STANDALONE: "true",
|
|
||||||
G_IS_BROWSER: "false",
|
|
||||||
G_HAVE_ASSERT: "true",
|
G_HAVE_ASSERT: "true",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -18,8 +18,6 @@ const globalDefs = {
|
|||||||
"G_ALL_UI_IMAGES": JSON.stringify(getAllResourceImages()),
|
"G_ALL_UI_IMAGES": JSON.stringify(getAllResourceImages()),
|
||||||
|
|
||||||
"G_IS_RELEASE": "true",
|
"G_IS_RELEASE": "true",
|
||||||
"G_IS_STANDALONE": "true",
|
|
||||||
"G_IS_BROWSER": "false",
|
|
||||||
"G_HAVE_ASSERT": "false",
|
"G_HAVE_ASSERT": "false",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
16
package.json
16
package.json
@ -12,13 +12,13 @@
|
|||||||
"lint": "(eslint . && tsc && tsc -p src) || (tsc && tsc -p src) || tsc -p src",
|
"lint": "(eslint . && tsc && tsc -p src) || (tsc && tsc -p src) || tsc -p src",
|
||||||
"prettier-all": "prettier --write src/**/*.* && prettier --write gulp/**/*.*",
|
"prettier-all": "prettier --write src/**/*.* && prettier --write gulp/**/*.*",
|
||||||
"buildTypes": "tsc src/js/application.js --declaration --allowJs --emitDeclarationOnly --skipLibCheck --out types.js",
|
"buildTypes": "tsc src/js/application.js --declaration --allowJs --emitDeclarationOnly --skipLibCheck --out types.js",
|
||||||
"package-win32-x64": "gulp --cwd gulp package.standalone-steam.win32-x64",
|
"package-win32-x64": "gulp --cwd gulp package.standalone.win32-x64",
|
||||||
"package-win32-arm64": "gulp --cwd gulp package.standalone-steam.win32-arm64",
|
"package-win32-arm64": "gulp --cwd gulp package.standalone.win32-arm64",
|
||||||
"package-linux-x64": "gulp --cwd gulp package.standalone-steam.linux-x64",
|
"package-linux-x64": "gulp --cwd gulp package.standalone.linux-x64",
|
||||||
"package-linux-arm64": "gulp --cwd gulp package.standalone-steam.linux-arm64",
|
"package-linux-arm64": "gulp --cwd gulp package.standalone.linux-arm64",
|
||||||
"package-darwin-x64": "gulp --cwd gulp package.standalone-steam.darwin-x64",
|
"package-darwin-x64": "gulp --cwd gulp package.standalone.darwin-x64",
|
||||||
"package-darwin-arm64": "gulp --cwd gulp package.standalone-steam.darwin-arm64",
|
"package-darwin-arm64": "gulp --cwd gulp package.standalone.darwin-arm64",
|
||||||
"package-all": "gulp --cwd gulp package.standalone-steam.all"
|
"package-all": "gulp --cwd gulp package.standalone.all"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": "^6.10.2",
|
"ajv": "^6.10.2",
|
||||||
@ -95,4 +95,4 @@
|
|||||||
"yaml": "^1.10.0",
|
"yaml": "^1.10.0",
|
||||||
"yarn": "^1.22.4"
|
"yarn": "^1.22.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5,16 +5,15 @@ import { GameState } from "./core/game_state";
|
|||||||
import { GLOBAL_APP, setGlobalApp } from "./core/globals";
|
import { GLOBAL_APP, setGlobalApp } from "./core/globals";
|
||||||
import { InputDistributor } from "./core/input_distributor";
|
import { InputDistributor } from "./core/input_distributor";
|
||||||
import { Loader } from "./core/loader";
|
import { Loader } from "./core/loader";
|
||||||
import { createLogger, logSection } from "./core/logging";
|
import { createLogger } from "./core/logging";
|
||||||
import { StateManager } from "./core/state_manager";
|
import { StateManager } from "./core/state_manager";
|
||||||
import { TrackedState } from "./core/tracked_state";
|
import { TrackedState } from "./core/tracked_state";
|
||||||
import { getPlatformName, waitNextFrame } from "./core/utils";
|
import { getPlatformName, waitNextFrame } from "./core/utils";
|
||||||
import { Vector } from "./core/vector";
|
import { Vector } from "./core/vector";
|
||||||
import { NoAchievementProvider } from "./platform/browser/no_achievement_provider";
|
import { NoAchievementProvider } from "./platform/no_achievement_provider";
|
||||||
import { SoundImplBrowser } from "./platform/browser/sound";
|
import { Sound } from "./platform/sound";
|
||||||
import { PlatformWrapperImplBrowser } from "./platform/browser/wrapper";
|
import { Storage } from "./platform/storage";
|
||||||
import { PlatformWrapperImplElectron } from "./platform/electron/wrapper";
|
import { PlatformWrapperImplElectron } from "./platform/wrapper";
|
||||||
import { PlatformWrapperInterface } from "./platform/wrapper";
|
|
||||||
import { ApplicationSettings } from "./profile/application_settings";
|
import { ApplicationSettings } from "./profile/application_settings";
|
||||||
import { SavegameManager } from "./savegame/savegame_manager";
|
import { SavegameManager } from "./savegame/savegame_manager";
|
||||||
import { AboutState } from "./states/about";
|
import { AboutState } from "./states/about";
|
||||||
@ -35,7 +34,6 @@ import { ModsState } from "./states/mods";
|
|||||||
/**
|
/**
|
||||||
* @typedef {import("./platform/achievement_provider").AchievementProviderInterface} AchievementProviderInterface
|
* @typedef {import("./platform/achievement_provider").AchievementProviderInterface} AchievementProviderInterface
|
||||||
* @typedef {import("./platform/sound").SoundInterface} SoundInterface
|
* @typedef {import("./platform/sound").SoundInterface} SoundInterface
|
||||||
* @typedef {import("./platform/storage").StorageInterface} StorageInterface
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const logger = createLogger("application");
|
const logger = createLogger("application");
|
||||||
@ -89,19 +87,12 @@ export class Application {
|
|||||||
|
|
||||||
// Platform dependent stuff
|
// Platform dependent stuff
|
||||||
|
|
||||||
/** @type {StorageInterface} */
|
this.storage = new Storage(this);
|
||||||
this.storage = null;
|
|
||||||
|
|
||||||
/** @type {SoundInterface} */
|
this.platformWrapper = new PlatformWrapperImplElectron(this);
|
||||||
this.sound = null;
|
|
||||||
|
|
||||||
/** @type {PlatformWrapperInterface} */
|
this.sound = new Sound(this);
|
||||||
this.platformWrapper = null;
|
this.achievementProvider = new NoAchievementProvider(this);
|
||||||
|
|
||||||
/** @type {AchievementProviderInterface} */
|
|
||||||
this.achievementProvider = null;
|
|
||||||
|
|
||||||
this.initPlatformDependentInstances();
|
|
||||||
|
|
||||||
// Track if the window is focused (only relevant for browser)
|
// Track if the window is focused (only relevant for browser)
|
||||||
this.focused = true;
|
this.focused = true;
|
||||||
@ -151,22 +142,6 @@ export class Application {
|
|||||||
MOD_SIGNALS.appBooted.dispatch();
|
MOD_SIGNALS.appBooted.dispatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes all platform instances
|
|
||||||
*/
|
|
||||||
initPlatformDependentInstances() {
|
|
||||||
logger.log("Creating platform dependent instances (standalone=", G_IS_STANDALONE, ")");
|
|
||||||
|
|
||||||
if (G_IS_STANDALONE) {
|
|
||||||
this.platformWrapper = new PlatformWrapperImplElectron(this);
|
|
||||||
} else {
|
|
||||||
this.platformWrapper = new PlatformWrapperImplBrowser(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sound = new SoundImplBrowser(this);
|
|
||||||
this.achievementProvider = new NoAchievementProvider(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers all game states
|
* Registers all game states
|
||||||
*/
|
*/
|
||||||
@ -312,18 +287,7 @@ export class Application {
|
|||||||
/**
|
/**
|
||||||
* Internal before-unload handler
|
* Internal before-unload handler
|
||||||
*/
|
*/
|
||||||
onBeforeUnload(event) {
|
onBeforeUnload(event) {}
|
||||||
logSection("BEFORE UNLOAD HANDLER", "#f77");
|
|
||||||
const currentState = this.stateMgr.getCurrentState();
|
|
||||||
|
|
||||||
if (!G_IS_DEV && currentState && currentState.getHasUnloadConfirmation()) {
|
|
||||||
if (!G_IS_STANDALONE) {
|
|
||||||
// Need to show a "Are you sure you want to exit"
|
|
||||||
event.preventDefault();
|
|
||||||
event.returnValue = "Are you sure you want to exit?";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deinitializes the application
|
* Deinitializes the application
|
||||||
|
|||||||
@ -30,10 +30,8 @@ const INGAME_ASSETS = {
|
|||||||
css: ["async-resources.css"],
|
css: ["async-resources.css"],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (G_IS_STANDALONE) {
|
MAIN_MENU_ASSETS.sounds = [...Array.from(Object.values(MUSIC)), ...Array.from(Object.values(SOUNDS))];
|
||||||
MAIN_MENU_ASSETS.sounds = [...Array.from(Object.values(MUSIC)), ...Array.from(Object.values(SOUNDS))];
|
INGAME_ASSETS.sounds = [];
|
||||||
INGAME_ASSETS.sounds = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const LOADER_TIMEOUT_PER_RESOURCE = 180000;
|
const LOADER_TIMEOUT_PER_RESOURCE = 180000;
|
||||||
|
|
||||||
@ -170,28 +168,13 @@ export class BackgroundResourcesLoader {
|
|||||||
* Shows an error when a resource failed to load and allows to reload the game
|
* Shows an error when a resource failed to load and allows to reload the game
|
||||||
*/
|
*/
|
||||||
showLoaderError(dialogs, err) {
|
showLoaderError(dialogs, err) {
|
||||||
if (G_IS_STANDALONE) {
|
dialogs
|
||||||
dialogs
|
.showWarning(
|
||||||
.showWarning(
|
T.dialogs.resourceLoadFailed.title,
|
||||||
T.dialogs.resourceLoadFailed.title,
|
T.dialogs.resourceLoadFailed.descSteamDemo + "<br>" + err,
|
||||||
T.dialogs.resourceLoadFailed.descSteamDemo + "<br>" + err,
|
["retry"]
|
||||||
["retry"]
|
)
|
||||||
)
|
.retry.add(() => window.location.reload());
|
||||||
.retry.add(() => window.location.reload());
|
|
||||||
} else {
|
|
||||||
dialogs
|
|
||||||
.showWarning(
|
|
||||||
T.dialogs.resourceLoadFailed.title,
|
|
||||||
T.dialogs.resourceLoadFailed.descWeb.replace(
|
|
||||||
"<demoOnSteamLinkText>",
|
|
||||||
`<a href="https://get.shapez.io/resource_timeout" target="_blank">${T.dialogs.resourceLoadFailed.demoLinkText}</a>`
|
|
||||||
) +
|
|
||||||
"<br>" +
|
|
||||||
err,
|
|
||||||
["retry"]
|
|
||||||
)
|
|
||||||
.retry.add(() => window.location.reload());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
preloadWithProgress(src, progressHandler) {
|
preloadWithProgress(src, progressHandler) {
|
||||||
|
|||||||
@ -21,8 +21,6 @@ export const BUILD_OPTIONS = {
|
|||||||
APP_ENVIRONMENT: G_APP_ENVIRONMENT,
|
APP_ENVIRONMENT: G_APP_ENVIRONMENT,
|
||||||
IS_DEV: G_IS_DEV,
|
IS_DEV: G_IS_DEV,
|
||||||
IS_RELEASE: G_IS_RELEASE,
|
IS_RELEASE: G_IS_RELEASE,
|
||||||
IS_BROWSER: G_IS_BROWSER,
|
|
||||||
IS_STANDALONE: G_IS_STANDALONE,
|
|
||||||
BUILD_TIME: G_BUILD_TIME,
|
BUILD_TIME: G_BUILD_TIME,
|
||||||
BUILD_COMMIT_HASH: G_BUILD_COMMIT_HASH,
|
BUILD_COMMIT_HASH: G_BUILD_COMMIT_HASH,
|
||||||
BUILD_VERSION: G_BUILD_VERSION,
|
BUILD_VERSION: G_BUILD_VERSION,
|
||||||
|
|||||||
@ -4,15 +4,10 @@ const bigNumberSuffixTranslationKeys = ["thousands", "millions", "billions", "tr
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a platform name
|
* Returns a platform name
|
||||||
* @returns {"android" | "browser" | "ios" | "standalone" | "unknown"}
|
* @returns {"standalone"}
|
||||||
*/
|
*/
|
||||||
export function getPlatformName() {
|
export function getPlatformName() {
|
||||||
if (G_IS_STANDALONE) {
|
return "standalone";
|
||||||
return "standalone";
|
|
||||||
} else if (G_IS_BROWSER) {
|
|
||||||
return "browser";
|
|
||||||
}
|
|
||||||
return "unknown";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -421,41 +416,7 @@ export function removeAllChildren(elem) {
|
|||||||
* Returns if the game supports this browser
|
* Returns if the game supports this browser
|
||||||
*/
|
*/
|
||||||
export function isSupportedBrowser() {
|
export function isSupportedBrowser() {
|
||||||
// please note,
|
return true;
|
||||||
// that IE11 now returns undefined again for window.chrome
|
|
||||||
// and new Opera 30 outputs true for window.chrome
|
|
||||||
// but needs to check if window.opr is not undefined
|
|
||||||
// and new IE Edge outputs to true now for window.chrome
|
|
||||||
// and if not iOS Chrome check
|
|
||||||
// so use the below updated condition
|
|
||||||
|
|
||||||
if (G_IS_STANDALONE) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
var isChromium = window.chrome;
|
|
||||||
var winNav = window.navigator;
|
|
||||||
var vendorName = winNav.vendor;
|
|
||||||
// @ts-ignore
|
|
||||||
var isIEedge = winNav.userAgent.indexOf("Edge") > -1;
|
|
||||||
var isIOSChrome = winNav.userAgent.match("CriOS");
|
|
||||||
|
|
||||||
if (isIOSChrome) {
|
|
||||||
// is Google Chrome on IOS
|
|
||||||
return false;
|
|
||||||
} else if (
|
|
||||||
isChromium !== null &&
|
|
||||||
typeof isChromium !== "undefined" &&
|
|
||||||
vendorName === "Google Inc." &&
|
|
||||||
isIEedge === false
|
|
||||||
) {
|
|
||||||
// is Google Chrome
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
// not Google Chrome
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export class HUDUnlockNotification extends BaseHUDPart {
|
|||||||
}
|
}
|
||||||
|
|
||||||
shouldPauseGame() {
|
shouldPauseGame() {
|
||||||
return !G_IS_STANDALONE && this.visible;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
createElements(parent) {
|
createElements(parent) {
|
||||||
|
|||||||
184
src/js/globals.d.ts
vendored
184
src/js/globals.d.ts
vendored
@ -1,10 +1,13 @@
|
|||||||
// Globals defined by webpack
|
// Globals defined by webpack
|
||||||
|
|
||||||
declare const G_IS_DEV: boolean;
|
declare const G_IS_DEV: boolean;
|
||||||
declare function assert(condition: boolean | object | string, ...errorMessage: string[]): asserts condition;
|
declare function assert(
|
||||||
|
condition: boolean | object | string,
|
||||||
|
...errorMessage: string[]
|
||||||
|
): asserts condition;
|
||||||
declare function assertAlways(
|
declare function assertAlways(
|
||||||
condition: boolean | object | string,
|
condition: boolean | object | string,
|
||||||
...errorMessage: string[]
|
...errorMessage: string[]
|
||||||
): asserts condition;
|
): asserts condition;
|
||||||
|
|
||||||
declare const abstract: void;
|
declare const abstract: void;
|
||||||
@ -12,8 +15,6 @@ declare const abstract: void;
|
|||||||
declare const G_APP_ENVIRONMENT: string;
|
declare const G_APP_ENVIRONMENT: string;
|
||||||
declare const G_HAVE_ASSERT: boolean;
|
declare const G_HAVE_ASSERT: boolean;
|
||||||
declare const G_BUILD_TIME: number;
|
declare const G_BUILD_TIME: number;
|
||||||
declare const G_IS_STANDALONE: boolean;
|
|
||||||
declare const G_IS_BROWSER: boolean;
|
|
||||||
|
|
||||||
declare const G_BUILD_COMMIT_HASH: string;
|
declare const G_BUILD_COMMIT_HASH: string;
|
||||||
declare const G_BUILD_VERSION: string;
|
declare const G_BUILD_VERSION: string;
|
||||||
@ -26,146 +27,163 @@ declare const ipcRenderer: any;
|
|||||||
|
|
||||||
// Polyfills
|
// Polyfills
|
||||||
declare interface String {
|
declare interface String {
|
||||||
replaceAll(search: string, replacement: string): string;
|
replaceAll(search: string, replacement: string): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare interface ImportMeta {
|
||||||
|
webpackContext(
|
||||||
|
request: string,
|
||||||
|
options?: {
|
||||||
|
recursive?: boolean;
|
||||||
|
regExp?: RegExp;
|
||||||
|
include?: RegExp;
|
||||||
|
exclude?: RegExp;
|
||||||
|
preload?: boolean | number;
|
||||||
|
prefetch?: boolean | number;
|
||||||
|
chunkName?: string;
|
||||||
|
exports?: string | string[][];
|
||||||
|
mode?: "sync" | "eager" | "weak" | "lazy" | "lazy-once";
|
||||||
|
},
|
||||||
|
): webpack.Context;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface CanvasRenderingContext2D {
|
declare interface CanvasRenderingContext2D {
|
||||||
beginRoundedRect(x: number, y: number, w: number, h: number, r: number): void;
|
beginRoundedRect(x: number, y: number, w: number, h: number, r: number): void;
|
||||||
beginCircle(x: number, y: number, r: number): void;
|
beginCircle(x: number, y: number, r: number): void;
|
||||||
|
|
||||||
msImageSmoothingEnabled: boolean;
|
msImageSmoothingEnabled: boolean;
|
||||||
mozImageSmoothingEnabled: boolean;
|
mozImageSmoothingEnabled: boolean;
|
||||||
webkitImageSmoothingEnabled: boolean;
|
webkitImageSmoothingEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Just for compatibility with the shared code
|
// Just for compatibility with the shared code
|
||||||
declare interface Logger {
|
declare interface Logger {
|
||||||
log(...args);
|
log(...args);
|
||||||
warn(...args);
|
warn(...args);
|
||||||
info(...args);
|
info(...args);
|
||||||
error(...args);
|
error(...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface MobileAccessibility {
|
declare interface MobileAccessibility {
|
||||||
usePreferredTextZoom(boolean);
|
usePreferredTextZoom(boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface Window {
|
declare interface Window {
|
||||||
// Debugging
|
// Debugging
|
||||||
activeClickDetectors: Array<any>;
|
activeClickDetectors: Array<any>;
|
||||||
|
|
||||||
// Mods
|
// Mods
|
||||||
$shapez_registerMod: any;
|
$shapez_registerMod: any;
|
||||||
anyModLoaded: any;
|
anyModLoaded: any;
|
||||||
|
|
||||||
shapez: any;
|
shapez: any;
|
||||||
|
|
||||||
APP_ERROR_OCCURED?: boolean;
|
APP_ERROR_OCCURED?: boolean;
|
||||||
|
|
||||||
webkitRequestAnimationFrame();
|
webkitRequestAnimationFrame();
|
||||||
|
|
||||||
assert(condition: boolean, failureMessage: string);
|
assert(condition: boolean, failureMessage: string);
|
||||||
|
|
||||||
coreThreadLoadedCb();
|
coreThreadLoadedCb();
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface Navigator {
|
declare interface Navigator {
|
||||||
app: any;
|
app: any;
|
||||||
device: any;
|
device: any;
|
||||||
splashscreen: any;
|
splashscreen: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Webpack
|
// Webpack
|
||||||
declare interface WebpackContext {
|
declare interface WebpackContext {
|
||||||
keys(): Array<string>;
|
keys(): Array<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface NodeRequire {
|
declare interface NodeRequire {
|
||||||
context(src: string, flag: boolean, regexp: RegExp): WebpackContext;
|
context(src: string, flag: boolean, regexp: RegExp): WebpackContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface Object {
|
declare interface Object {
|
||||||
entries(obj: object): Array<[string, any]>;
|
entries(obj: object): Array<[string, any]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface Math {
|
declare interface Math {
|
||||||
radians(number): number;
|
radians(number): number;
|
||||||
degrees(number): number;
|
degrees(number): number;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare type Class<T = unknown> = new (...args: any[]) => T;
|
declare type Class<T = unknown> = new (...args: any[]) => T;
|
||||||
|
|
||||||
declare interface String {
|
declare interface String {
|
||||||
padStart(size: number, fill?: string): string;
|
padStart(size: number, fill?: string): string;
|
||||||
padEnd(size: number, fill: string): string;
|
padEnd(size: number, fill: string): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare interface SignalTemplate0 {
|
declare interface SignalTemplate0 {
|
||||||
add(receiver: () => string | void, scope: null | any);
|
add(receiver: () => string | void, scope: null | any);
|
||||||
dispatch(): string | void;
|
dispatch(): string | void;
|
||||||
remove(receiver: () => string | void);
|
remove(receiver: () => string | void);
|
||||||
removeAll();
|
removeAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
declare class TypedTrackedState<T> {
|
declare class TypedTrackedState<T> {
|
||||||
constructor(callbackMethod?: (value: T) => void, callbackScope?: any);
|
constructor(callbackMethod?: (value: T) => void, callbackScope?: any);
|
||||||
|
|
||||||
set(value: T, changeHandler?: (value: T) => void, changeScope?: any): void;
|
set(value: T, changeHandler?: (value: T) => void, changeScope?: any): void;
|
||||||
|
|
||||||
setSilent(value: any): void;
|
setSilent(value: any): void;
|
||||||
get(): T;
|
get(): T;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare type Layer = "regular" | "wires";
|
declare type Layer = "regular" | "wires";
|
||||||
declare type ItemType = "shape" | "color" | "boolean";
|
declare type ItemType = "shape" | "color" | "boolean";
|
||||||
|
|
||||||
declare module "worker-loader?inline=true&fallback=false!*" {
|
declare module "worker-loader?inline=true&fallback=false!*" {
|
||||||
class WebpackWorker extends Worker {
|
class WebpackWorker extends Worker {
|
||||||
constructor();
|
constructor();
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WebpackWorker;
|
export default WebpackWorker;
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSX type support - https://www.typescriptlang.org/docs/handbook/jsx.html
|
// JSX type support - https://www.typescriptlang.org/docs/handbook/jsx.html
|
||||||
// modified from https://stackoverflow.com/a/68238924
|
// modified from https://stackoverflow.com/a/68238924
|
||||||
declare namespace JSX {
|
declare namespace JSX {
|
||||||
/**
|
/**
|
||||||
* The return type of a JSX expression.
|
* The return type of a JSX expression.
|
||||||
*
|
*
|
||||||
* In reality, Fragments can return arbitrary values, but we ignore this for convenience.
|
* In reality, Fragments can return arbitrary values, but we ignore this for convenience.
|
||||||
*/
|
*/
|
||||||
type Element = HTMLElement;
|
type Element = HTMLElement;
|
||||||
/**
|
/**
|
||||||
* Key-value list of intrinsic element names and their allowed properties.
|
* Key-value list of intrinsic element names and their allowed properties.
|
||||||
*
|
*
|
||||||
* Because children are treated as a property, the Node type cannot be excluded from the index signature.
|
* Because children are treated as a property, the Node type cannot be excluded from the index signature.
|
||||||
*/
|
*/
|
||||||
type IntrinsicElements = {
|
type IntrinsicElements = {
|
||||||
[K in keyof HTMLElementTagNameMap]: {
|
[K in keyof HTMLElementTagNameMap]: {
|
||||||
children?: Node | Node[];
|
children?: Node | Node[];
|
||||||
[k: string]: Node | Node[] | string | number | boolean;
|
[k: string]: Node | Node[] | string | number | boolean;
|
||||||
};
|
|
||||||
};
|
};
|
||||||
/**
|
};
|
||||||
* The property of the attributes object storing the children.
|
/**
|
||||||
*/
|
* The property of the attributes object storing the children.
|
||||||
type ElementChildrenAttribute = { children: unknown };
|
*/
|
||||||
|
type ElementChildrenAttribute = { children: unknown };
|
||||||
|
|
||||||
// The following do not have special meaning to TypeScript.
|
// The following do not have special meaning to TypeScript.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An attributes object.
|
* An attributes object.
|
||||||
*/
|
*/
|
||||||
type Props = { [k: string]: unknown };
|
type Props = { [k: string]: unknown };
|
||||||
/**
|
/**
|
||||||
* A functional component requiring attributes to match `T`.
|
* A functional component requiring attributes to match `T`.
|
||||||
*/
|
*/
|
||||||
type Component<T extends Props> = {
|
type Component<T extends Props> = {
|
||||||
(props: T): Element;
|
(props: T): Element;
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* A child of a JSX element.
|
* A child of a JSX element.
|
||||||
*/
|
*/
|
||||||
type Node = Element | string | boolean | null | undefined;
|
type Node = Element | string | boolean | null | undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,8 +63,4 @@ function bootApp() {
|
|||||||
app.boot();
|
app.boot();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (G_IS_STANDALONE) {
|
window.addEventListener("load", bootApp);
|
||||||
window.addEventListener("load", bootApp);
|
|
||||||
} else {
|
|
||||||
bootApp();
|
|
||||||
}
|
|
||||||
|
|||||||
@ -3,8 +3,7 @@ import { Application } from "../application";
|
|||||||
/* typehints:end */
|
/* typehints:end */
|
||||||
import { globalConfig } from "../core/config";
|
import { globalConfig } from "../core/config";
|
||||||
import { createLogger } from "../core/logging";
|
import { createLogger } from "../core/logging";
|
||||||
import { StorageImplBrowserIndexedDB } from "../platform/browser/storage_indexed_db";
|
import { Storage } from "../platform/storage";
|
||||||
import { StorageImplElectron } from "../platform/electron/storage";
|
|
||||||
import { FILE_NOT_FOUND } from "../platform/storage";
|
import { FILE_NOT_FOUND } from "../platform/storage";
|
||||||
import { Mod } from "./mod";
|
import { Mod } from "./mod";
|
||||||
import { ModInterface } from "./mod_interface";
|
import { ModInterface } from "./mod_interface";
|
||||||
@ -105,7 +104,7 @@ export class ModLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exposeExports() {
|
exposeExports() {
|
||||||
if (G_IS_DEV || G_IS_STANDALONE) {
|
if (G_IS_DEV) {
|
||||||
let exports = {};
|
let exports = {};
|
||||||
const modules = import.meta.webpackContext("../", {
|
const modules = import.meta.webpackContext("../", {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
@ -140,24 +139,14 @@ export class ModLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async initMods() {
|
async initMods() {
|
||||||
if (!G_IS_STANDALONE && !G_IS_DEV) {
|
|
||||||
this.initialized = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a storage for reading mod settings
|
// Create a storage for reading mod settings
|
||||||
const storage = G_IS_STANDALONE
|
const storage = new Storage(this.app);
|
||||||
? new StorageImplElectron(this.app)
|
|
||||||
: new StorageImplBrowserIndexedDB(this.app);
|
|
||||||
await storage.initialize();
|
await storage.initialize();
|
||||||
|
|
||||||
LOG.log("hook:init", this.app, this.app.storage);
|
LOG.log("hook:init", this.app, this.app.storage);
|
||||||
this.exposeExports();
|
this.exposeExports();
|
||||||
|
|
||||||
let mods = [];
|
let mods = await ipcRenderer.invoke("get-mods");
|
||||||
if (G_IS_STANDALONE) {
|
|
||||||
mods = await ipcRenderer.invoke("get-mods");
|
|
||||||
}
|
|
||||||
if (G_IS_DEV && globalConfig.debug.externalModUrl) {
|
if (G_IS_DEV && globalConfig.debug.externalModUrl) {
|
||||||
const modURLs = Array.isArray(globalConfig.debug.externalModUrl)
|
const modURLs = Array.isArray(globalConfig.debug.externalModUrl)
|
||||||
? globalConfig.debug.externalModUrl
|
? globalConfig.debug.externalModUrl
|
||||||
|
|||||||
@ -1,207 +0,0 @@
|
|||||||
import { MusicInstanceInterface, SoundInstanceInterface, SoundInterface, MUSIC, SOUNDS } from "../sound";
|
|
||||||
import { createLogger } from "../../core/logging";
|
|
||||||
import { globalConfig } from "../../core/config";
|
|
||||||
|
|
||||||
import { Howl, Howler } from "howler";
|
|
||||||
|
|
||||||
const logger = createLogger("sound/browser");
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import sprites from "../../built-temp/sfx.json";
|
|
||||||
|
|
||||||
class SoundSpritesContainer {
|
|
||||||
constructor() {
|
|
||||||
this.howl = null;
|
|
||||||
|
|
||||||
this.loadingPromise = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
load() {
|
|
||||||
if (this.loadingPromise) {
|
|
||||||
return this.loadingPromise;
|
|
||||||
}
|
|
||||||
return (this.loadingPromise = new Promise(resolve => {
|
|
||||||
this.howl = new Howl({
|
|
||||||
src: "res/sounds/sfx.mp3",
|
|
||||||
sprite: sprites.sprite,
|
|
||||||
autoplay: false,
|
|
||||||
loop: false,
|
|
||||||
volume: 0,
|
|
||||||
preload: true,
|
|
||||||
pool: 20,
|
|
||||||
onload: () => {
|
|
||||||
resolve();
|
|
||||||
},
|
|
||||||
onloaderror: (id, err) => {
|
|
||||||
logger.warn("SFX failed to load:", id, err);
|
|
||||||
this.howl = null;
|
|
||||||
resolve();
|
|
||||||
},
|
|
||||||
onplayerror: (id, err) => {
|
|
||||||
logger.warn("SFX failed to play:", id, err);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
play(volume, key) {
|
|
||||||
if (this.howl) {
|
|
||||||
const instance = this.howl.play(key);
|
|
||||||
this.howl.volume(volume, instance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deinitialize() {
|
|
||||||
if (this.howl) {
|
|
||||||
this.howl.unload();
|
|
||||||
this.howl = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WrappedSoundInstance extends SoundInstanceInterface {
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {SoundSpritesContainer} spriteContainer
|
|
||||||
* @param {string} key
|
|
||||||
*/
|
|
||||||
constructor(spriteContainer, key) {
|
|
||||||
super(key, "sfx.mp3");
|
|
||||||
this.spriteContainer = spriteContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @returns {Promise<void>} */
|
|
||||||
load() {
|
|
||||||
return this.spriteContainer.load();
|
|
||||||
}
|
|
||||||
|
|
||||||
play(volume) {
|
|
||||||
this.spriteContainer.play(volume, this.key);
|
|
||||||
}
|
|
||||||
|
|
||||||
deinitialize() {
|
|
||||||
return this.spriteContainer.deinitialize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MusicInstance extends MusicInstanceInterface {
|
|
||||||
constructor(key, url) {
|
|
||||||
super(key, url);
|
|
||||||
this.howl = null;
|
|
||||||
this.instance = null;
|
|
||||||
this.playing = false;
|
|
||||||
}
|
|
||||||
load() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.howl = new Howl({
|
|
||||||
src: "res/sounds/music/" + this.url + ".mp3",
|
|
||||||
autoplay: false,
|
|
||||||
loop: true,
|
|
||||||
html5: true,
|
|
||||||
volume: 1,
|
|
||||||
preload: true,
|
|
||||||
pool: 2,
|
|
||||||
|
|
||||||
onunlock: () => {
|
|
||||||
if (this.playing) {
|
|
||||||
logger.log("Playing music after manual unlock");
|
|
||||||
this.play();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onload: () => {
|
|
||||||
resolve();
|
|
||||||
},
|
|
||||||
onloaderror: (id, err) => {
|
|
||||||
logger.warn(this, "Music", this.url, "failed to load:", id, err);
|
|
||||||
this.howl = null;
|
|
||||||
resolve();
|
|
||||||
},
|
|
||||||
|
|
||||||
onplayerror: (id, err) => {
|
|
||||||
logger.warn(this, "Music", this.url, "failed to play:", id, err);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
if (this.howl && this.instance) {
|
|
||||||
this.playing = false;
|
|
||||||
this.howl.pause(this.instance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isPlaying() {
|
|
||||||
return this.playing;
|
|
||||||
}
|
|
||||||
|
|
||||||
play(volume) {
|
|
||||||
if (this.howl) {
|
|
||||||
this.playing = true;
|
|
||||||
this.howl.volume(volume);
|
|
||||||
if (this.instance) {
|
|
||||||
this.howl.play(this.instance);
|
|
||||||
} else {
|
|
||||||
this.instance = this.howl.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setVolume(volume) {
|
|
||||||
if (this.howl) {
|
|
||||||
this.howl.volume(volume);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deinitialize() {
|
|
||||||
if (this.howl) {
|
|
||||||
this.howl.unload();
|
|
||||||
this.howl = null;
|
|
||||||
this.instance = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SoundImplBrowser extends SoundInterface {
|
|
||||||
constructor(app) {
|
|
||||||
Howler.mobileAutoEnable = true;
|
|
||||||
Howler.autoUnlock = true;
|
|
||||||
Howler.autoSuspend = false;
|
|
||||||
Howler.html5PoolSize = 20;
|
|
||||||
Howler.pos(0, 0, 0);
|
|
||||||
|
|
||||||
super(app, WrappedSoundInstance, MusicInstance);
|
|
||||||
}
|
|
||||||
|
|
||||||
initialize() {
|
|
||||||
// NOTICE: We override the initialize() method here with custom logic because
|
|
||||||
// we have a sound sprites instance
|
|
||||||
|
|
||||||
this.sfxHandle = new SoundSpritesContainer();
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
const keys = Object.values(SOUNDS);
|
|
||||||
keys.forEach(key => {
|
|
||||||
this.sounds[key] = new WrappedSoundInstance(this.sfxHandle, key);
|
|
||||||
});
|
|
||||||
for (const musicKey in MUSIC) {
|
|
||||||
const musicPath = MUSIC[musicKey];
|
|
||||||
const music = new this.musicClass(musicKey, musicPath);
|
|
||||||
this.music[musicPath] = music;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.musicVolume = this.app.settings.getAllSettings().musicVolume;
|
|
||||||
this.soundVolume = this.app.settings.getAllSettings().soundVolume;
|
|
||||||
|
|
||||||
if (G_IS_DEV && globalConfig.debug.disableMusic) {
|
|
||||||
this.musicVolume = 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
deinitialize() {
|
|
||||||
return super.deinitialize().then(() => Howler.unload());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,93 +0,0 @@
|
|||||||
import { FILE_NOT_FOUND, StorageInterface } from "../storage";
|
|
||||||
import { createLogger } from "../../core/logging";
|
|
||||||
|
|
||||||
const logger = createLogger("storage/browser");
|
|
||||||
|
|
||||||
const LOCAL_STORAGE_UNAVAILABLE = "local-storage-unavailable";
|
|
||||||
const LOCAL_STORAGE_NO_WRITE_PERMISSION = "local-storage-no-write-permission";
|
|
||||||
|
|
||||||
let randomDelay = () => 0;
|
|
||||||
|
|
||||||
if (G_IS_DEV) {
|
|
||||||
// Random delay for testing
|
|
||||||
// randomDelay = () => 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class StorageImplBrowser extends StorageInterface {
|
|
||||||
constructor(app) {
|
|
||||||
super(app);
|
|
||||||
this.currentBusyFilename = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
initialize() {
|
|
||||||
logger.error("Using localStorage, please update to a newer browser");
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
// Check for local storage availability in general
|
|
||||||
if (!window.localStorage) {
|
|
||||||
alert("Local storage is not available! Please upgrade to a newer browser!");
|
|
||||||
reject(LOCAL_STORAGE_UNAVAILABLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we can set and remove items
|
|
||||||
try {
|
|
||||||
window.localStorage.setItem("storage_availability_test", "1");
|
|
||||||
window.localStorage.removeItem("storage_availability_test");
|
|
||||||
} catch (e) {
|
|
||||||
alert(
|
|
||||||
"It seems we don't have permission to write to local storage! Please update your browsers settings or use a different browser!"
|
|
||||||
);
|
|
||||||
reject(LOCAL_STORAGE_NO_WRITE_PERMISSION);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setTimeout(resolve, 0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
writeFileAsync(filename, contents) {
|
|
||||||
if (this.currentBusyFilename === filename) {
|
|
||||||
logger.warn("Attempt to write", filename, "while write process is not finished!");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentBusyFilename = filename;
|
|
||||||
window.localStorage.setItem(filename, contents);
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.currentBusyFilename = false;
|
|
||||||
resolve();
|
|
||||||
}, 0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
readFileAsync(filename) {
|
|
||||||
if (this.currentBusyFilename === filename) {
|
|
||||||
logger.warn("Attempt to read", filename, "while write progress on it is ongoing!");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const contents = window.localStorage.getItem(filename);
|
|
||||||
if (!contents) {
|
|
||||||
// File not found
|
|
||||||
setTimeout(() => reject(FILE_NOT_FOUND), randomDelay());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// File read, simulate delay
|
|
||||||
setTimeout(() => resolve(contents), 0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteFileAsync(filename) {
|
|
||||||
if (this.currentBusyFilename === filename) {
|
|
||||||
logger.warn("Attempt to delete", filename, "while write progres on it is ongoing!");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentBusyFilename = filename;
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
window.localStorage.removeItem(filename);
|
|
||||||
setTimeout(() => {
|
|
||||||
this.currentBusyFilename = false;
|
|
||||||
resolve();
|
|
||||||
}, 0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,154 +0,0 @@
|
|||||||
import { FILE_NOT_FOUND, StorageInterface } from "../storage";
|
|
||||||
import { createLogger } from "../../core/logging";
|
|
||||||
|
|
||||||
const logger = createLogger("storage/browserIDB");
|
|
||||||
|
|
||||||
const LOCAL_STORAGE_UNAVAILABLE = "local-storage-unavailable";
|
|
||||||
const LOCAL_STORAGE_NO_WRITE_PERMISSION = "local-storage-no-write-permission";
|
|
||||||
|
|
||||||
let randomDelay = () => 0;
|
|
||||||
|
|
||||||
if (G_IS_DEV) {
|
|
||||||
// Random delay for testing
|
|
||||||
// randomDelay = () => 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class StorageImplBrowserIndexedDB extends StorageInterface {
|
|
||||||
constructor(app) {
|
|
||||||
super(app);
|
|
||||||
this.currentBusyFilename = false;
|
|
||||||
|
|
||||||
/** @type {IDBDatabase} */
|
|
||||||
this.database = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
initialize() {
|
|
||||||
logger.log("Using indexed DB storage");
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const request = window.indexedDB.open("app_storage", 10);
|
|
||||||
request.onerror = event => {
|
|
||||||
logger.error("IDB error:", event);
|
|
||||||
alert(
|
|
||||||
"Sorry, it seems your browser has blocked the access to the storage system. This might be the case if you are browsing in private mode for example. I recommend to use google chrome or disable private browsing."
|
|
||||||
);
|
|
||||||
reject("Indexed DB access error");
|
|
||||||
};
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
request.onsuccess = event => resolve(event.target.result);
|
|
||||||
|
|
||||||
request.onupgradeneeded = /** @type {IDBVersionChangeEvent} */ event => {
|
|
||||||
/** @type {IDBDatabase} */
|
|
||||||
// @ts-ignore
|
|
||||||
const database = event.target.result;
|
|
||||||
|
|
||||||
const objectStore = database.createObjectStore("files", {
|
|
||||||
keyPath: "filename",
|
|
||||||
});
|
|
||||||
|
|
||||||
objectStore.createIndex("filename", "filename", { unique: true });
|
|
||||||
|
|
||||||
objectStore.transaction.onerror = event => {
|
|
||||||
logger.error("IDB transaction error:", event);
|
|
||||||
reject("Indexed DB transaction error during migration, check console output.");
|
|
||||||
};
|
|
||||||
|
|
||||||
objectStore.transaction.oncomplete = event => {
|
|
||||||
logger.log("Object store completely initialized");
|
|
||||||
resolve(database);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}).then(database => {
|
|
||||||
this.database = database;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
writeFileAsync(filename, contents) {
|
|
||||||
if (this.currentBusyFilename === filename) {
|
|
||||||
logger.warn("Attempt to write", filename, "while write process is not finished!");
|
|
||||||
}
|
|
||||||
if (!this.database) {
|
|
||||||
return Promise.reject("Storage not ready");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentBusyFilename = filename;
|
|
||||||
const transaction = this.database.transaction(["files"], "readwrite");
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
transaction.oncomplete = () => {
|
|
||||||
this.currentBusyFilename = null;
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
transaction.onerror = error => {
|
|
||||||
this.currentBusyFilename = null;
|
|
||||||
logger.error("Error while writing", filename, ":", error);
|
|
||||||
reject(error);
|
|
||||||
};
|
|
||||||
|
|
||||||
const store = transaction.objectStore("files");
|
|
||||||
store.put({
|
|
||||||
filename,
|
|
||||||
contents,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
readFileAsync(filename) {
|
|
||||||
if (!this.database) {
|
|
||||||
return Promise.reject("Storage not ready");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentBusyFilename = filename;
|
|
||||||
const transaction = this.database.transaction(["files"], "readonly");
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const store = transaction.objectStore("files");
|
|
||||||
const request = store.get(filename);
|
|
||||||
|
|
||||||
request.onsuccess = event => {
|
|
||||||
this.currentBusyFilename = null;
|
|
||||||
if (!request.result) {
|
|
||||||
reject(FILE_NOT_FOUND);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve(request.result.contents);
|
|
||||||
};
|
|
||||||
|
|
||||||
request.onerror = error => {
|
|
||||||
this.currentBusyFilename = null;
|
|
||||||
logger.error("Error while reading", filename, ":", error);
|
|
||||||
reject(error);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteFileAsync(filename) {
|
|
||||||
if (this.currentBusyFilename === filename) {
|
|
||||||
logger.warn("Attempt to delete", filename, "while write progres on it is ongoing!");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.database) {
|
|
||||||
return Promise.reject("Storage not ready");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentBusyFilename = filename;
|
|
||||||
const transaction = this.database.transaction(["files"], "readwrite");
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
transaction.oncomplete = () => {
|
|
||||||
this.currentBusyFilename = null;
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
transaction.onerror = error => {
|
|
||||||
this.currentBusyFilename = null;
|
|
||||||
logger.error("Error while deleting", filename, ":", error);
|
|
||||||
reject(error);
|
|
||||||
};
|
|
||||||
|
|
||||||
const store = transaction.objectStore("files");
|
|
||||||
store.delete(filename);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,102 +0,0 @@
|
|||||||
import { globalConfig, IS_MOBILE } from "../../core/config";
|
|
||||||
import { createLogger } from "../../core/logging";
|
|
||||||
import { clamp } from "../../core/utils";
|
|
||||||
import { SteamAchievementProvider } from "../electron/steam_achievement_provider";
|
|
||||||
import { PlatformWrapperInterface } from "../wrapper";
|
|
||||||
import { NoAchievementProvider } from "./no_achievement_provider";
|
|
||||||
import { StorageImplBrowser } from "./storage";
|
|
||||||
import { StorageImplBrowserIndexedDB } from "./storage_indexed_db";
|
|
||||||
|
|
||||||
const logger = createLogger("platform/browser");
|
|
||||||
|
|
||||||
export class PlatformWrapperImplBrowser extends PlatformWrapperInterface {
|
|
||||||
initialize() {
|
|
||||||
return this.detectStorageImplementation()
|
|
||||||
.then(() => this.initializeAchievementProvider())
|
|
||||||
.then(() => super.initialize());
|
|
||||||
}
|
|
||||||
|
|
||||||
detectStorageImplementation() {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
logger.log("Detecting storage");
|
|
||||||
|
|
||||||
if (!window.indexedDB) {
|
|
||||||
logger.log("Indexed DB not supported");
|
|
||||||
this.app.storage = new StorageImplBrowser(this.app);
|
|
||||||
resolve();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try accessing the indexedb
|
|
||||||
let request;
|
|
||||||
try {
|
|
||||||
request = window.indexedDB.open("indexeddb_feature_detection", 1);
|
|
||||||
} catch (ex) {
|
|
||||||
logger.warn("Error while opening indexed db:", ex);
|
|
||||||
this.app.storage = new StorageImplBrowser(this.app);
|
|
||||||
resolve();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
request.onerror = err => {
|
|
||||||
logger.log("Indexed DB can *not* be accessed: ", err);
|
|
||||||
logger.log("Using fallback to local storage");
|
|
||||||
this.app.storage = new StorageImplBrowser(this.app);
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
request.onsuccess = () => {
|
|
||||||
logger.log("Indexed DB *can* be accessed");
|
|
||||||
this.app.storage = new StorageImplBrowserIndexedDB(this.app);
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getId() {
|
|
||||||
return "browser";
|
|
||||||
}
|
|
||||||
|
|
||||||
getUiScale() {
|
|
||||||
if (IS_MOBILE) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const avgDims = Math.min(this.app.screenWidth, this.app.screenHeight);
|
|
||||||
return clamp((avgDims / 1000.0) * 1.9, 0.1, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
getSupportsRestart() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
getTouchPanStrength() {
|
|
||||||
return IS_MOBILE ? 1 : 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
openExternalLink(url, force = false) {
|
|
||||||
logger.log("Opening external:", url);
|
|
||||||
window.open(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
performRestart() {
|
|
||||||
logger.log("Performing restart");
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeAchievementProvider() {
|
|
||||||
if (G_IS_DEV && globalConfig.debug.testAchievements) {
|
|
||||||
this.app.achievementProvider = new SteamAchievementProvider(this.app);
|
|
||||||
|
|
||||||
return this.app.achievementProvider.initialize().catch(err => {
|
|
||||||
logger.error("Failed to initialize achievement provider, disabling:", err);
|
|
||||||
|
|
||||||
this.app.achievementProvider = new NoAchievementProvider(this.app);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.app.achievementProvider.initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
exitApp() {
|
|
||||||
// Can not exit app
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,140 +0,0 @@
|
|||||||
/* typehints:start */
|
|
||||||
import { Application } from "../../application";
|
|
||||||
import { GameRoot } from "../../game/root";
|
|
||||||
/* typehints:end */
|
|
||||||
|
|
||||||
import { createLogger } from "../../core/logging";
|
|
||||||
import { ACHIEVEMENTS, AchievementCollection, AchievementProviderInterface } from "../achievement_provider";
|
|
||||||
|
|
||||||
const logger = createLogger("achievements/steam");
|
|
||||||
|
|
||||||
const ACHIEVEMENT_IDS = {
|
|
||||||
[ACHIEVEMENTS.belt500Tiles]: "belt_500_tiles",
|
|
||||||
[ACHIEVEMENTS.blueprint100k]: "blueprint_100k",
|
|
||||||
[ACHIEVEMENTS.blueprint1m]: "blueprint_1m",
|
|
||||||
[ACHIEVEMENTS.completeLvl26]: "complete_lvl_26",
|
|
||||||
[ACHIEVEMENTS.cutShape]: "cut_shape",
|
|
||||||
[ACHIEVEMENTS.darkMode]: "dark_mode",
|
|
||||||
[ACHIEVEMENTS.destroy1000]: "destroy_1000",
|
|
||||||
[ACHIEVEMENTS.irrelevantShape]: "irrelevant_shape",
|
|
||||||
[ACHIEVEMENTS.level100]: "level_100",
|
|
||||||
[ACHIEVEMENTS.level50]: "level_50",
|
|
||||||
[ACHIEVEMENTS.logoBefore18]: "logo_before_18",
|
|
||||||
[ACHIEVEMENTS.mam]: "mam",
|
|
||||||
[ACHIEVEMENTS.mapMarkers15]: "map_markers_15",
|
|
||||||
[ACHIEVEMENTS.openWires]: "open_wires",
|
|
||||||
[ACHIEVEMENTS.oldLevel17]: "old_level_17",
|
|
||||||
[ACHIEVEMENTS.noBeltUpgradesUntilBp]: "no_belt_upgrades_until_bp",
|
|
||||||
[ACHIEVEMENTS.noInverseRotater]: "no_inverse_rotator", // [sic]
|
|
||||||
[ACHIEVEMENTS.paintShape]: "paint_shape",
|
|
||||||
[ACHIEVEMENTS.place5000Wires]: "place_5000_wires",
|
|
||||||
[ACHIEVEMENTS.placeBlueprint]: "place_blueprint",
|
|
||||||
[ACHIEVEMENTS.placeBp1000]: "place_bp_1000",
|
|
||||||
[ACHIEVEMENTS.play1h]: "play_1h",
|
|
||||||
[ACHIEVEMENTS.play10h]: "play_10h",
|
|
||||||
[ACHIEVEMENTS.play20h]: "play_20h",
|
|
||||||
[ACHIEVEMENTS.produceLogo]: "produce_logo",
|
|
||||||
[ACHIEVEMENTS.produceMsLogo]: "produce_ms_logo",
|
|
||||||
[ACHIEVEMENTS.produceRocket]: "produce_rocket",
|
|
||||||
[ACHIEVEMENTS.rotateShape]: "rotate_shape",
|
|
||||||
[ACHIEVEMENTS.speedrunBp30]: "speedrun_bp_30",
|
|
||||||
[ACHIEVEMENTS.speedrunBp60]: "speedrun_bp_60",
|
|
||||||
[ACHIEVEMENTS.speedrunBp120]: "speedrun_bp_120",
|
|
||||||
[ACHIEVEMENTS.stack4Layers]: "stack_4_layers",
|
|
||||||
[ACHIEVEMENTS.stackShape]: "stack_shape",
|
|
||||||
[ACHIEVEMENTS.store100Unique]: "store_100_unique",
|
|
||||||
[ACHIEVEMENTS.storeShape]: "store_shape",
|
|
||||||
[ACHIEVEMENTS.throughputBp25]: "throughput_bp_25",
|
|
||||||
[ACHIEVEMENTS.throughputBp50]: "throughput_bp_50",
|
|
||||||
[ACHIEVEMENTS.throughputLogo25]: "throughput_logo_25",
|
|
||||||
[ACHIEVEMENTS.throughputLogo50]: "throughput_logo_50",
|
|
||||||
[ACHIEVEMENTS.throughputRocket10]: "throughput_rocket_10",
|
|
||||||
[ACHIEVEMENTS.throughputRocket20]: "throughput_rocket_20",
|
|
||||||
[ACHIEVEMENTS.trash1000]: "trash_1000",
|
|
||||||
[ACHIEVEMENTS.unlockWires]: "unlock_wires",
|
|
||||||
[ACHIEVEMENTS.upgradesTier5]: "upgrades_tier_5",
|
|
||||||
[ACHIEVEMENTS.upgradesTier8]: "upgrades_tier_8",
|
|
||||||
};
|
|
||||||
|
|
||||||
export class SteamAchievementProvider extends AchievementProviderInterface {
|
|
||||||
/** @param {Application} app */
|
|
||||||
constructor(app) {
|
|
||||||
super(app);
|
|
||||||
|
|
||||||
this.initialized = false;
|
|
||||||
this.collection = new AchievementCollection(this.activate.bind(this));
|
|
||||||
|
|
||||||
if (G_IS_DEV) {
|
|
||||||
for (let key in ACHIEVEMENT_IDS) {
|
|
||||||
assert(this.collection.map.has(key), "Key not found in collection: " + key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.log("Collection created with", this.collection.map.size, "achievements");
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @returns {boolean} */
|
|
||||||
hasAchievements() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {GameRoot} root
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
onLoad(root) {
|
|
||||||
this.root = root;
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.collection = new AchievementCollection(this.activate.bind(this));
|
|
||||||
this.collection.initialize(root);
|
|
||||||
|
|
||||||
logger.log("Initialized", this.collection.map.size, "relevant achievements");
|
|
||||||
return Promise.resolve();
|
|
||||||
} catch (err) {
|
|
||||||
logger.error("Failed to initialize the collection");
|
|
||||||
return Promise.reject(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @returns {Promise<void>} */
|
|
||||||
initialize() {
|
|
||||||
if (!G_IS_STANDALONE) {
|
|
||||||
logger.warn("Steam unavailable. Achievements won't sync.");
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
return ipcRenderer.invoke("steam:is-initialized").then(initialized => {
|
|
||||||
this.initialized = initialized;
|
|
||||||
|
|
||||||
if (!this.initialized) {
|
|
||||||
logger.warn("Steam failed to intialize. Achievements won't sync.");
|
|
||||||
} else {
|
|
||||||
logger.log("Steam achievement provider initialized");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} key
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
activate(key) {
|
|
||||||
let promise;
|
|
||||||
|
|
||||||
if (!this.initialized) {
|
|
||||||
promise = Promise.resolve();
|
|
||||||
} else {
|
|
||||||
promise = ipcRenderer.invoke("steam:activate-achievement", ACHIEVEMENT_IDS[key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return promise
|
|
||||||
.then(() => {
|
|
||||||
logger.log("Achievement activated:", key);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
logger.error("Failed to activate achievement:", key, err);
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
import { FILE_NOT_FOUND, StorageInterface } from "../storage";
|
|
||||||
|
|
||||||
export class StorageImplElectron extends StorageInterface {
|
|
||||||
constructor(app) {
|
|
||||||
super(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
initialize() {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
writeFileAsync(filename, contents) {
|
|
||||||
return ipcRenderer.invoke("fs-job", {
|
|
||||||
type: "write",
|
|
||||||
filename,
|
|
||||||
contents,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
readFileAsync(filename) {
|
|
||||||
return ipcRenderer
|
|
||||||
.invoke("fs-job", {
|
|
||||||
type: "read",
|
|
||||||
filename,
|
|
||||||
})
|
|
||||||
.then(res => {
|
|
||||||
if (res && res.error === FILE_NOT_FOUND) {
|
|
||||||
throw FILE_NOT_FOUND;
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteFileAsync(filename) {
|
|
||||||
return ipcRenderer.invoke("fs-job", {
|
|
||||||
type: "delete",
|
|
||||||
filename,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,98 +0,0 @@
|
|||||||
import { NoAchievementProvider } from "../browser/no_achievement_provider";
|
|
||||||
import { PlatformWrapperImplBrowser } from "../browser/wrapper";
|
|
||||||
import { createLogger } from "../../core/logging";
|
|
||||||
import { StorageImplElectron } from "./storage";
|
|
||||||
import { SteamAchievementProvider } from "./steam_achievement_provider";
|
|
||||||
import { PlatformWrapperInterface } from "../wrapper";
|
|
||||||
|
|
||||||
const logger = createLogger("electron-wrapper");
|
|
||||||
|
|
||||||
export class PlatformWrapperImplElectron extends PlatformWrapperImplBrowser {
|
|
||||||
initialize() {
|
|
||||||
this.dlcs = {
|
|
||||||
puzzle: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.steamOverlayCanvasFix = document.createElement("canvas");
|
|
||||||
this.steamOverlayCanvasFix.width = 1;
|
|
||||||
this.steamOverlayCanvasFix.height = 1;
|
|
||||||
this.steamOverlayCanvasFix.id = "steamOverlayCanvasFix";
|
|
||||||
|
|
||||||
this.steamOverlayContextFix = this.steamOverlayCanvasFix.getContext("2d");
|
|
||||||
document.documentElement.appendChild(this.steamOverlayCanvasFix);
|
|
||||||
|
|
||||||
this.app.ticker.frameEmitted.add(this.steamOverlayFixRedrawCanvas, this);
|
|
||||||
|
|
||||||
this.app.storage = new StorageImplElectron(this);
|
|
||||||
this.app.achievementProvider = new SteamAchievementProvider(this.app);
|
|
||||||
|
|
||||||
return this.initializeAchievementProvider()
|
|
||||||
.then(() => this.initializeDlcStatus())
|
|
||||||
.then(() => PlatformWrapperInterface.prototype.initialize.call(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
steamOverlayFixRedrawCanvas() {
|
|
||||||
this.steamOverlayContextFix.clearRect(0, 0, 1, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
getId() {
|
|
||||||
return "electron";
|
|
||||||
}
|
|
||||||
|
|
||||||
getSupportsRestart() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
openExternalLink(url) {
|
|
||||||
logger.log(this, "Opening external:", url);
|
|
||||||
window.open(url, "about:blank");
|
|
||||||
}
|
|
||||||
|
|
||||||
getSupportsAds() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
performRestart() {
|
|
||||||
logger.log(this, "Performing restart");
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeAchievementProvider() {
|
|
||||||
return this.app.achievementProvider.initialize().catch(err => {
|
|
||||||
logger.error("Failed to initialize achievement provider, disabling:", err);
|
|
||||||
|
|
||||||
this.app.achievementProvider = new NoAchievementProvider(this.app);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeDlcStatus() {
|
|
||||||
logger.log("Checking DLC ownership ...");
|
|
||||||
// @todo: Don't hardcode the app id
|
|
||||||
return ipcRenderer.invoke("steam:check-app-ownership", 1625400).then(
|
|
||||||
res => {
|
|
||||||
logger.log("Got DLC ownership:", res);
|
|
||||||
this.dlcs.puzzle = Boolean(res);
|
|
||||||
},
|
|
||||||
err => {
|
|
||||||
logger.error("Failed to get DLC ownership:", err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getSupportsFullscreen() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
setFullscreen(flag) {
|
|
||||||
ipcRenderer.send("set-fullscreen", flag);
|
|
||||||
}
|
|
||||||
|
|
||||||
getSupportsAppExit() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
exitApp() {
|
|
||||||
logger.log(this, "Sending app exit signal");
|
|
||||||
ipcRenderer.send("exit-app");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { AchievementProviderInterface } from "../achievement_provider";
|
import { AchievementProviderInterface } from "./achievement_provider";
|
||||||
|
|
||||||
export class NoAchievementProvider extends AchievementProviderInterface {
|
export class NoAchievementProvider extends AchievementProviderInterface {
|
||||||
hasAchievements() {
|
hasAchievements() {
|
||||||
@ -7,6 +7,10 @@ import { GameRoot } from "../game/root";
|
|||||||
import { newEmptyMap, clamp } from "../core/utils";
|
import { newEmptyMap, clamp } from "../core/utils";
|
||||||
import { createLogger } from "../core/logging";
|
import { createLogger } from "../core/logging";
|
||||||
import { globalConfig } from "../core/config";
|
import { globalConfig } from "../core/config";
|
||||||
|
import { Howl, Howler } from "howler";
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import sprites from "../built-temp/sfx.json";
|
||||||
|
|
||||||
const logger = createLogger("sound");
|
const logger = createLogger("sound");
|
||||||
|
|
||||||
@ -33,16 +37,12 @@ export const SOUNDS = {
|
|||||||
export const MUSIC = {
|
export const MUSIC = {
|
||||||
// The theme always depends on the standalone only, even if running the full
|
// The theme always depends on the standalone only, even if running the full
|
||||||
// version in the browser
|
// version in the browser
|
||||||
theme: G_IS_STANDALONE ? "theme-full" : "theme-short",
|
theme: "theme-full",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (G_IS_STANDALONE) {
|
MUSIC.menu = "menu";
|
||||||
MUSIC.menu = "menu";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (G_IS_STANDALONE) {
|
MUSIC.puzzle = "puzzle-full";
|
||||||
MUSIC.puzzle = "puzzle-full";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SoundInstanceInterface {
|
export class SoundInstanceInterface {
|
||||||
constructor(key, url) {
|
constructor(key, url) {
|
||||||
@ -292,3 +292,200 @@ export class SoundInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SoundSpritesContainer {
|
||||||
|
constructor() {
|
||||||
|
this.howl = null;
|
||||||
|
|
||||||
|
this.loadingPromise = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
load() {
|
||||||
|
if (this.loadingPromise) {
|
||||||
|
return this.loadingPromise;
|
||||||
|
}
|
||||||
|
return (this.loadingPromise = new Promise(resolve => {
|
||||||
|
this.howl = new Howl({
|
||||||
|
src: "res/sounds/sfx.mp3",
|
||||||
|
sprite: sprites.sprite,
|
||||||
|
autoplay: false,
|
||||||
|
loop: false,
|
||||||
|
volume: 0,
|
||||||
|
preload: true,
|
||||||
|
pool: 20,
|
||||||
|
onload: () => {
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
onloaderror: (id, err) => {
|
||||||
|
logger.warn("SFX failed to load:", id, err);
|
||||||
|
this.howl = null;
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
onplayerror: (id, err) => {
|
||||||
|
logger.warn("SFX failed to play:", id, err);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
play(volume, key) {
|
||||||
|
if (this.howl) {
|
||||||
|
const instance = this.howl.play(key);
|
||||||
|
this.howl.volume(volume, instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deinitialize() {
|
||||||
|
if (this.howl) {
|
||||||
|
this.howl.unload();
|
||||||
|
this.howl = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WrappedSoundInstance extends SoundInstanceInterface {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {SoundSpritesContainer} spriteContainer
|
||||||
|
* @param {string} key
|
||||||
|
*/
|
||||||
|
constructor(spriteContainer, key) {
|
||||||
|
super(key, "sfx.mp3");
|
||||||
|
this.spriteContainer = spriteContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {Promise<void>} */
|
||||||
|
load() {
|
||||||
|
return this.spriteContainer.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
play(volume) {
|
||||||
|
this.spriteContainer.play(volume, this.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
deinitialize() {
|
||||||
|
return this.spriteContainer.deinitialize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MusicInstance extends MusicInstanceInterface {
|
||||||
|
constructor(key, url) {
|
||||||
|
super(key, url);
|
||||||
|
this.howl = null;
|
||||||
|
this.instance = null;
|
||||||
|
this.playing = false;
|
||||||
|
}
|
||||||
|
load() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.howl = new Howl({
|
||||||
|
src: "res/sounds/music/" + this.url + ".mp3",
|
||||||
|
autoplay: false,
|
||||||
|
loop: true,
|
||||||
|
html5: true,
|
||||||
|
volume: 1,
|
||||||
|
preload: true,
|
||||||
|
pool: 2,
|
||||||
|
|
||||||
|
onunlock: () => {
|
||||||
|
if (this.playing) {
|
||||||
|
logger.log("Playing music after manual unlock");
|
||||||
|
this.play();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onload: () => {
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
onloaderror: (id, err) => {
|
||||||
|
logger.warn(this, "Music", this.url, "failed to load:", id, err);
|
||||||
|
this.howl = null;
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
|
||||||
|
onplayerror: (id, err) => {
|
||||||
|
logger.warn(this, "Music", this.url, "failed to play:", id, err);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
if (this.howl && this.instance) {
|
||||||
|
this.playing = false;
|
||||||
|
this.howl.pause(this.instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isPlaying() {
|
||||||
|
return this.playing;
|
||||||
|
}
|
||||||
|
|
||||||
|
play(volume) {
|
||||||
|
if (this.howl) {
|
||||||
|
this.playing = true;
|
||||||
|
this.howl.volume(volume);
|
||||||
|
if (this.instance) {
|
||||||
|
this.howl.play(this.instance);
|
||||||
|
} else {
|
||||||
|
this.instance = this.howl.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setVolume(volume) {
|
||||||
|
if (this.howl) {
|
||||||
|
this.howl.volume(volume);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deinitialize() {
|
||||||
|
if (this.howl) {
|
||||||
|
this.howl.unload();
|
||||||
|
this.howl = null;
|
||||||
|
this.instance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Sound extends SoundInterface {
|
||||||
|
constructor(app) {
|
||||||
|
Howler.mobileAutoEnable = true;
|
||||||
|
Howler.autoUnlock = true;
|
||||||
|
Howler.autoSuspend = false;
|
||||||
|
Howler.html5PoolSize = 20;
|
||||||
|
Howler.pos(0, 0, 0);
|
||||||
|
|
||||||
|
super(app, WrappedSoundInstance, MusicInstance);
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
// NOTICE: We override the initialize() method here with custom logic because
|
||||||
|
// we have a sound sprites instance
|
||||||
|
|
||||||
|
this.sfxHandle = new SoundSpritesContainer();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const keys = Object.values(SOUNDS);
|
||||||
|
keys.forEach(key => {
|
||||||
|
this.sounds[key] = new WrappedSoundInstance(this.sfxHandle, key);
|
||||||
|
});
|
||||||
|
for (const musicKey in MUSIC) {
|
||||||
|
const musicPath = MUSIC[musicKey];
|
||||||
|
const music = new this.musicClass(musicKey, musicPath);
|
||||||
|
this.music[musicPath] = music;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.musicVolume = this.app.settings.getAllSettings().musicVolume;
|
||||||
|
this.soundVolume = this.app.settings.getAllSettings().soundVolume;
|
||||||
|
|
||||||
|
if (G_IS_DEV && globalConfig.debug.disableMusic) {
|
||||||
|
this.musicVolume = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
deinitialize() {
|
||||||
|
return super.deinitialize().then(() => Howler.unload());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { Application } from "../application";
|
|||||||
|
|
||||||
export const FILE_NOT_FOUND = "file_not_found";
|
export const FILE_NOT_FOUND = "file_not_found";
|
||||||
|
|
||||||
export class StorageInterface {
|
export class Storage {
|
||||||
constructor(app) {
|
constructor(app) {
|
||||||
/** @type {Application} */
|
/** @type {Application} */
|
||||||
this.app = app;
|
this.app = app;
|
||||||
@ -13,11 +13,9 @@ export class StorageInterface {
|
|||||||
/**
|
/**
|
||||||
* Initializes the storage
|
* Initializes the storage
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
* @abstract
|
|
||||||
*/
|
*/
|
||||||
initialize() {
|
initialize() {
|
||||||
abstract;
|
return Promise.resolve();
|
||||||
return Promise.reject();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -25,22 +23,33 @@ export class StorageInterface {
|
|||||||
* @param {string} filename
|
* @param {string} filename
|
||||||
* @param {string} contents
|
* @param {string} contents
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
* @abstract
|
|
||||||
*/
|
*/
|
||||||
writeFileAsync(filename, contents) {
|
writeFileAsync(filename, contents) {
|
||||||
abstract;
|
return ipcRenderer.invoke("fs-job", {
|
||||||
return Promise.reject();
|
type: "write",
|
||||||
|
filename,
|
||||||
|
contents,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads a string asynchronously. Returns Promise<FILE_NOT_FOUND> if file was not found.
|
* Reads a string asynchronously. Returns Promise<FILE_NOT_FOUND> if file was not found.
|
||||||
* @param {string} filename
|
* @param {string} filename
|
||||||
* @returns {Promise<string>}
|
* @returns {Promise<string>}
|
||||||
* @abstract
|
|
||||||
*/
|
*/
|
||||||
readFileAsync(filename) {
|
readFileAsync(filename) {
|
||||||
abstract;
|
return ipcRenderer
|
||||||
return Promise.reject();
|
.invoke("fs-job", {
|
||||||
|
type: "read",
|
||||||
|
filename,
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (res && res.error === FILE_NOT_FOUND) {
|
||||||
|
throw FILE_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -49,7 +58,9 @@ export class StorageInterface {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
deleteFileAsync(filename) {
|
deleteFileAsync(filename) {
|
||||||
// Default implementation does not allow deleting files
|
return ipcRenderer.invoke("fs-job", {
|
||||||
return Promise.reject();
|
type: "delete",
|
||||||
|
filename,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,30 +3,60 @@ import { Application } from "../application";
|
|||||||
/* typehints:end */
|
/* typehints:end */
|
||||||
|
|
||||||
import { IS_MOBILE } from "../core/config";
|
import { IS_MOBILE } from "../core/config";
|
||||||
|
import { NoAchievementProvider } from "./no_achievement_provider";
|
||||||
|
import { createLogger } from "../core/logging";
|
||||||
|
import { clamp } from "../core/utils";
|
||||||
|
|
||||||
export class PlatformWrapperInterface {
|
const logger = createLogger("electron-wrapper");
|
||||||
|
|
||||||
|
export class PlatformWrapperImplElectron {
|
||||||
constructor(app) {
|
constructor(app) {
|
||||||
/** @type {Application} */
|
/** @type {Application} */
|
||||||
this.app = app;
|
this.app = app;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @returns {string} */
|
initialize() {
|
||||||
|
this.dlcs = {
|
||||||
|
puzzle: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.steamOverlayCanvasFix = document.createElement("canvas");
|
||||||
|
this.steamOverlayCanvasFix.width = 1;
|
||||||
|
this.steamOverlayCanvasFix.height = 1;
|
||||||
|
this.steamOverlayCanvasFix.id = "steamOverlayCanvasFix";
|
||||||
|
|
||||||
|
this.steamOverlayContextFix = this.steamOverlayCanvasFix.getContext("2d");
|
||||||
|
document.documentElement.appendChild(this.steamOverlayCanvasFix);
|
||||||
|
|
||||||
|
this.app.ticker.frameEmitted.add(this.steamOverlayFixRedrawCanvas, this);
|
||||||
|
|
||||||
|
return this.initializeAchievementProvider()
|
||||||
|
.then(() => this.initializeDlcStatus())
|
||||||
|
.then(() => {
|
||||||
|
document.documentElement.classList.add("p-" + this.getId());
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
steamOverlayFixRedrawCanvas() {
|
||||||
|
this.steamOverlayContextFix.clearRect(0, 0, 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
getId() {
|
getId() {
|
||||||
abstract;
|
return "electron";
|
||||||
return "unknown-platform";
|
}
|
||||||
|
|
||||||
|
getSupportsRestart() {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the UI scale, called on every resize
|
* Attempt to open an external url
|
||||||
* @returns {number} */
|
* @param {string} url
|
||||||
getUiScale() {
|
*/
|
||||||
return 1;
|
openExternalLink(url) {
|
||||||
}
|
logger.log(this, "Opening external:", url);
|
||||||
|
window.open(url, "about:blank");
|
||||||
/** @returns {boolean} */
|
|
||||||
getSupportsRestart() {
|
|
||||||
abstract;
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -36,10 +66,86 @@ export class PlatformWrapperInterface {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @returns {Promise<void>} */
|
/**
|
||||||
initialize() {
|
* Should return if this platform supports ads at all
|
||||||
document.documentElement.classList.add("p-" + this.getId());
|
*/
|
||||||
return Promise.resolve();
|
getSupportsAds() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to restart the app
|
||||||
|
*/
|
||||||
|
performRestart() {
|
||||||
|
logger.log(this, "Performing restart");
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeAchievementProvider() {
|
||||||
|
return this.app.achievementProvider.initialize().catch(err => {
|
||||||
|
logger.error("Failed to initialize achievement provider, disabling:", err);
|
||||||
|
|
||||||
|
this.app.achievementProvider = new NoAchievementProvider(this.app);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeDlcStatus() {
|
||||||
|
logger.log("Checking DLC ownership ...");
|
||||||
|
// @todo: Don't hardcode the app id
|
||||||
|
return ipcRenderer.invoke("steam:check-app-ownership", 1625400).then(
|
||||||
|
res => {
|
||||||
|
logger.log("Got DLC ownership:", res);
|
||||||
|
this.dlcs.puzzle = Boolean(res);
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
logger.error("Failed to get DLC ownership:", err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the UI scale, called on every resize
|
||||||
|
* @returns {number} */
|
||||||
|
getUiScale() {
|
||||||
|
if (IS_MOBILE) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const avgDims = Math.min(this.app.screenWidth, this.app.screenHeight);
|
||||||
|
return clamp((avgDims / 1000.0) * 1.9, 0.1, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether this platform supports a toggleable fullscreen
|
||||||
|
*/
|
||||||
|
getSupportsFullscreen() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should set the apps fullscreen state to the desired state
|
||||||
|
* @param {boolean} flag
|
||||||
|
*/
|
||||||
|
setFullscreen(flag) {
|
||||||
|
ipcRenderer.send("set-fullscreen", flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSupportsAppExit() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Attempts to quit the app
|
||||||
|
*/
|
||||||
|
exitApp() {
|
||||||
|
logger.log(this, "Sending app exit signal");
|
||||||
|
ipcRenderer.send("exit-app");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this platform supports a keyboard
|
||||||
|
*/
|
||||||
|
getSupportsKeyboard() {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -61,67 +167,4 @@ export class PlatformWrapperInterface {
|
|||||||
getScreenScale() {
|
getScreenScale() {
|
||||||
return Math.min(window.innerWidth, window.innerHeight) / 1024.0;
|
return Math.min(window.innerWidth, window.innerHeight) / 1024.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Should return if this platform supports ads at all
|
|
||||||
*/
|
|
||||||
getSupportsAds() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempt to open an external url
|
|
||||||
* @param {string} url
|
|
||||||
* @param {boolean=} force Whether to always open the url even if not allowed
|
|
||||||
* @abstract
|
|
||||||
*/
|
|
||||||
openExternalLink(url, force = false) {
|
|
||||||
abstract;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempt to restart the app
|
|
||||||
* @abstract
|
|
||||||
*/
|
|
||||||
performRestart() {
|
|
||||||
abstract;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether this platform supports a toggleable fullscreen
|
|
||||||
*/
|
|
||||||
getSupportsFullscreen() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Should set the apps fullscreen state to the desired state
|
|
||||||
* @param {boolean} flag
|
|
||||||
* @abstract
|
|
||||||
*/
|
|
||||||
setFullscreen(flag) {
|
|
||||||
abstract;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether this platform supports quitting the app
|
|
||||||
*/
|
|
||||||
getSupportsAppExit() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts to quit the app
|
|
||||||
* @abstract
|
|
||||||
*/
|
|
||||||
exitApp() {
|
|
||||||
abstract;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether this platform supports a keyboard
|
|
||||||
*/
|
|
||||||
getSupportsKeyboard() {
|
|
||||||
return !IS_MOBILE;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -189,7 +189,7 @@ function initializeSettings() {
|
|||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* @param {Application} app
|
* @param {Application} app
|
||||||
*/ app => G_IS_STANDALONE
|
*/ app => true
|
||||||
),
|
),
|
||||||
|
|
||||||
new BoolSetting(
|
new BoolSetting(
|
||||||
@ -288,7 +288,7 @@ function initializeSettings() {
|
|||||||
class SettingsStorage {
|
class SettingsStorage {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.uiScale = "regular";
|
this.uiScale = "regular";
|
||||||
this.fullscreen = G_IS_STANDALONE;
|
this.fullscreen = true;
|
||||||
|
|
||||||
this.soundVolume = 1.0;
|
this.soundVolume = 1.0;
|
||||||
this.musicVolume = 1.0;
|
this.musicVolume = 1.0;
|
||||||
|
|||||||
@ -16,7 +16,6 @@ import {
|
|||||||
} from "../core/utils";
|
} from "../core/utils";
|
||||||
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
|
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
|
||||||
import { MODS } from "../mods/modloader";
|
import { MODS } from "../mods/modloader";
|
||||||
import { PlatformWrapperImplElectron } from "../platform/electron/wrapper";
|
|
||||||
import { Savegame } from "../savegame/savegame";
|
import { Savegame } from "../savegame/savegame";
|
||||||
import { T } from "../translations";
|
import { T } from "../translations";
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,7 @@ export class ModsState extends TextualGameState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get modsSupported() {
|
get modsSupported() {
|
||||||
return G_IS_STANDALONE || G_IS_DEV;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
internalGetFullHtml() {
|
internalGetFullHtml() {
|
||||||
@ -23,15 +23,11 @@ export class ModsState extends TextualGameState {
|
|||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
${
|
${
|
||||||
this.modsSupported && MODS.mods.length > 0
|
MODS.mods.length > 0
|
||||||
? `<button class="styledButton browseMods">${T.mods.browseMods}</button>`
|
? `<button class="styledButton browseMods">${T.mods.browseMods}</button>`
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
${
|
<button class="styledButton openModsFolder">${T.mods.openFolder}</button>
|
||||||
this.modsSupported
|
|
||||||
? `<button class="styledButton openModsFolder">${T.mods.openFolder}</button>`
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>`;
|
</div>`;
|
||||||
@ -45,18 +41,6 @@ export class ModsState extends TextualGameState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getMainContentHTML() {
|
getMainContentHTML() {
|
||||||
if (!this.modsSupported) {
|
|
||||||
return `
|
|
||||||
<div class="noModSupport">
|
|
||||||
|
|
||||||
<p>${T.mods.noModSupport}</p>
|
|
||||||
<br>
|
|
||||||
<button class="styledButton browseMods">${T.mods.browseMods}</button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MODS.mods.length === 0) {
|
if (MODS.mods.length === 0) {
|
||||||
return `
|
return `
|
||||||
|
|
||||||
@ -121,10 +105,6 @@ export class ModsState extends TextualGameState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openModsFolder() {
|
openModsFolder() {
|
||||||
if (!G_IS_STANDALONE) {
|
|
||||||
this.dialogs.showWarning(T.global.error, T.mods.folderOnlyStandalone);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ipcRenderer.invoke("open-mods-folder");
|
ipcRenderer.invoke("open-mods-folder");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import { createLogger } from "../core/logging";
|
|||||||
import { getLogoSprite, timeoutPromise } from "../core/utils";
|
import { getLogoSprite, timeoutPromise } from "../core/utils";
|
||||||
import { getRandomHint } from "../game/hints";
|
import { getRandomHint } from "../game/hints";
|
||||||
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
|
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
|
||||||
import { PlatformWrapperImplBrowser } from "../platform/browser/wrapper";
|
|
||||||
import { autoDetectLanguageId, T, updateApplicationLanguage } from "../translations";
|
import { autoDetectLanguageId, T, updateApplicationLanguage } from "../translations";
|
||||||
|
|
||||||
const logger = createLogger("state/preload");
|
const logger = createLogger("state/preload");
|
||||||
@ -74,27 +73,6 @@ export class PreloadState extends GameState {
|
|||||||
.then(() => this.setStatus("Creating platform wrapper", 3))
|
.then(() => this.setStatus("Creating platform wrapper", 3))
|
||||||
.then(() => this.app.platformWrapper.initialize())
|
.then(() => this.app.platformWrapper.initialize())
|
||||||
|
|
||||||
.then(() => this.setStatus("Initializing local storage", 6))
|
|
||||||
.then(() => {
|
|
||||||
const wrapper = this.app.platformWrapper;
|
|
||||||
if (wrapper instanceof PlatformWrapperImplBrowser) {
|
|
||||||
try {
|
|
||||||
window.localStorage.setItem("local_storage_test", "1");
|
|
||||||
window.localStorage.removeItem("local_storage_test");
|
|
||||||
} catch (ex) {
|
|
||||||
logger.error("Failed to read/write local storage:", ex);
|
|
||||||
return new Promise(() => {
|
|
||||||
alert(
|
|
||||||
"Your brower does not support thirdparty cookies or you have disabled it in your security settings.\n\n" +
|
|
||||||
"In Chrome this setting is called 'Block third-party cookies and site data'.\n\n" +
|
|
||||||
"Please allow third party cookies and then reload the page."
|
|
||||||
);
|
|
||||||
// Never return
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
.then(() => this.setStatus("Creating storage", 9))
|
.then(() => this.setStatus("Creating storage", 9))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return this.app.storage.initialize();
|
return this.app.storage.initialize();
|
||||||
@ -172,10 +150,6 @@ export class PreloadState extends GameState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!G_IS_STANDALONE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.app.storage
|
return this.app.storage
|
||||||
.readFileAsync("lastversion.bin")
|
.readFileAsync("lastversion.bin")
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
|||||||
@ -1,19 +1,24 @@
|
|||||||
{
|
{
|
||||||
"extends": ["@tsconfig/strictest/tsconfig"],
|
"extends": [
|
||||||
"include": ["./js/**/*"],
|
"@tsconfig/strictest/tsconfig"
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
"./js/**/*"
|
||||||
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"module": "es2022",
|
"module": "es2022",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
"target": "ES2022",
|
||||||
/* JSX Compilation */
|
/* JSX Compilation */
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./js/*"]
|
"@/*": [
|
||||||
|
"./js/*"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"jsxImportSource": "@",
|
"jsxImportSource": "@",
|
||||||
|
|
||||||
// remove when comfortable
|
// remove when comfortable
|
||||||
"exactOptionalPropertyTypes": false,
|
"exactOptionalPropertyTypes": false,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
@ -23,6 +28,6 @@
|
|||||||
"noUncheckedIndexedAccess": false,
|
"noUncheckedIndexedAccess": false,
|
||||||
"strictNullChecks": false,
|
"strictNullChecks": false,
|
||||||
// eslint warns for this
|
// eslint warns for this
|
||||||
"noUnusedLocals": true
|
"noUnusedLocals": true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user