1
0
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:
Даниїл Григор'єв 2024-06-20 02:12:52 +03:00 committed by GitHub
commit e142c1211f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 511 additions and 1353 deletions

View File

@ -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

View File

@ -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();
}

View File

@ -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,
};

View File

@ -1 +0,0 @@
1318690

View File

@ -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",
}, },

View File

@ -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";

View File

@ -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"]);

View File

@ -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",
}; };

View File

@ -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",
}; };

View File

@ -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"
} }
} }

View File

@ -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

View File

@ -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) {

View File

@ -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,

View File

@ -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;
}
} }
/** /**

View File

@ -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
View File

@ -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;
} }

View File

@ -63,8 +63,4 @@ function bootApp() {
app.boot(); app.boot();
} }
if (G_IS_STANDALONE) { window.addEventListener("load", bootApp);
window.addEventListener("load", bootApp);
} else {
bootApp();
}

View File

@ -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

View File

@ -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());
}
}

View File

@ -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);
});
}
}

View File

@ -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);
});
}
}

View File

@ -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
}
}

View File

@ -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;
});
}
}

View File

@ -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,
});
}
}

View File

@ -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");
}
}

View File

@ -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() {

View File

@ -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());
}
}

View File

@ -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,
});
} }
} }

View File

@ -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;
}
} }

View File

@ -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;

View File

@ -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";

View File

@ -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");
} }

View File

@ -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 => {

View File

@ -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,
} }
} }