From 6662cb26940ed4c9b563da787af8facffe8325e7 Mon Sep 17 00:00:00 2001 From: cody-ferguson Date: Wed, 19 Jun 2024 13:47:50 -0500 Subject: [PATCH 1/3] Remove all references of "G_IS_STANDALONE" --- gulp/webpack.config.js | 2 - gulp/webpack.production.config.js | 2 - src/js/application.js | 45 +---- src/js/core/background_resources_loader.js | 35 +--- src/js/core/globals.js | 2 - src/js/core/utils.js | 45 +---- src/js/game/hud/parts/unlock_notification.js | 2 +- src/js/globals.d.ts | 167 +++++++++--------- src/js/main.js | 6 +- src/js/mods/modloader.js | 16 +- .../electron/steam_achievement_provider.js | 5 - src/js/platform/sound.js | 10 +- src/js/profile/application_settings.js | 4 +- src/js/states/mods.js | 26 +-- src/js/states/preload.js | 4 - 15 files changed, 114 insertions(+), 257 deletions(-) diff --git a/gulp/webpack.config.js b/gulp/webpack.config.js index 596680e2..cac0161b 100644 --- a/gulp/webpack.config.js +++ b/gulp/webpack.config.js @@ -17,8 +17,6 @@ const globalDefs = { G_ALL_UI_IMAGES: JSON.stringify(getAllResourceImages()), G_IS_RELEASE: "false", - G_IS_STANDALONE: "true", - G_IS_BROWSER: "false", G_HAVE_ASSERT: "true", }; diff --git a/gulp/webpack.production.config.js b/gulp/webpack.production.config.js index c10b4dae..4962843a 100644 --- a/gulp/webpack.production.config.js +++ b/gulp/webpack.production.config.js @@ -18,8 +18,6 @@ const globalDefs = { "G_ALL_UI_IMAGES": JSON.stringify(getAllResourceImages()), "G_IS_RELEASE": "true", - "G_IS_STANDALONE": "true", - "G_IS_BROWSER": "false", "G_HAVE_ASSERT": "false", }; diff --git a/src/js/application.js b/src/js/application.js index 68dbc100..90b197b5 100644 --- a/src/js/application.js +++ b/src/js/application.js @@ -5,16 +5,14 @@ import { GameState } from "./core/game_state"; import { GLOBAL_APP, setGlobalApp } from "./core/globals"; import { InputDistributor } from "./core/input_distributor"; import { Loader } from "./core/loader"; -import { createLogger, logSection } from "./core/logging"; +import { createLogger } from "./core/logging"; import { StateManager } from "./core/state_manager"; import { TrackedState } from "./core/tracked_state"; import { getPlatformName, waitNextFrame } from "./core/utils"; import { Vector } from "./core/vector"; import { NoAchievementProvider } from "./platform/browser/no_achievement_provider"; import { SoundImplBrowser } from "./platform/browser/sound"; -import { PlatformWrapperImplBrowser } from "./platform/browser/wrapper"; import { PlatformWrapperImplElectron } from "./platform/electron/wrapper"; -import { PlatformWrapperInterface } from "./platform/wrapper"; import { ApplicationSettings } from "./profile/application_settings"; import { SavegameManager } from "./savegame/savegame_manager"; import { AboutState } from "./states/about"; @@ -92,16 +90,10 @@ export class Application { /** @type {StorageInterface} */ this.storage = null; - /** @type {SoundInterface} */ - this.sound = null; + this.platformWrapper = new PlatformWrapperImplElectron(this); - /** @type {PlatformWrapperInterface} */ - this.platformWrapper = null; - - /** @type {AchievementProviderInterface} */ - this.achievementProvider = null; - - this.initPlatformDependentInstances(); + this.sound = new SoundImplBrowser(this); + this.achievementProvider = new NoAchievementProvider(this); // Track if the window is focused (only relevant for browser) this.focused = true; @@ -151,22 +143,6 @@ export class Application { 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 */ @@ -312,18 +288,7 @@ export class Application { /** * Internal before-unload handler */ - 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?"; - } - } - } + onBeforeUnload(event) {} /** * Deinitializes the application diff --git a/src/js/core/background_resources_loader.js b/src/js/core/background_resources_loader.js index d967a04a..38584d56 100644 --- a/src/js/core/background_resources_loader.js +++ b/src/js/core/background_resources_loader.js @@ -30,10 +30,8 @@ const INGAME_ASSETS = { css: ["async-resources.css"], }; -if (G_IS_STANDALONE) { - MAIN_MENU_ASSETS.sounds = [...Array.from(Object.values(MUSIC)), ...Array.from(Object.values(SOUNDS))]; - INGAME_ASSETS.sounds = []; -} +MAIN_MENU_ASSETS.sounds = [...Array.from(Object.values(MUSIC)), ...Array.from(Object.values(SOUNDS))]; +INGAME_ASSETS.sounds = []; 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 */ showLoaderError(dialogs, err) { - if (G_IS_STANDALONE) { - dialogs - .showWarning( - T.dialogs.resourceLoadFailed.title, - T.dialogs.resourceLoadFailed.descSteamDemo + "
" + err, - ["retry"] - ) - .retry.add(() => window.location.reload()); - } else { - dialogs - .showWarning( - T.dialogs.resourceLoadFailed.title, - T.dialogs.resourceLoadFailed.descWeb.replace( - "", - `${T.dialogs.resourceLoadFailed.demoLinkText}` - ) + - "
" + - err, - ["retry"] - ) - .retry.add(() => window.location.reload()); - } + dialogs + .showWarning( + T.dialogs.resourceLoadFailed.title, + T.dialogs.resourceLoadFailed.descSteamDemo + "
" + err, + ["retry"] + ) + .retry.add(() => window.location.reload()); } preloadWithProgress(src, progressHandler) { diff --git a/src/js/core/globals.js b/src/js/core/globals.js index 71871b17..4e046824 100644 --- a/src/js/core/globals.js +++ b/src/js/core/globals.js @@ -21,8 +21,6 @@ export const BUILD_OPTIONS = { APP_ENVIRONMENT: G_APP_ENVIRONMENT, IS_DEV: G_IS_DEV, IS_RELEASE: G_IS_RELEASE, - IS_BROWSER: G_IS_BROWSER, - IS_STANDALONE: G_IS_STANDALONE, BUILD_TIME: G_BUILD_TIME, BUILD_COMMIT_HASH: G_BUILD_COMMIT_HASH, BUILD_VERSION: G_BUILD_VERSION, diff --git a/src/js/core/utils.js b/src/js/core/utils.js index aff10781..34213b7d 100644 --- a/src/js/core/utils.js +++ b/src/js/core/utils.js @@ -4,15 +4,10 @@ const bigNumberSuffixTranslationKeys = ["thousands", "millions", "billions", "tr /** * Returns a platform name - * @returns {"android" | "browser" | "ios" | "standalone" | "unknown"} + * @returns {"standalone"} */ export function getPlatformName() { - if (G_IS_STANDALONE) { - return "standalone"; - } else if (G_IS_BROWSER) { - return "browser"; - } - return "unknown"; + return "standalone"; } /** @@ -421,41 +416,7 @@ export function removeAllChildren(elem) { * Returns if the game supports this browser */ export function isSupportedBrowser() { - // please note, - // 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; - } + return true; } /** diff --git a/src/js/game/hud/parts/unlock_notification.js b/src/js/game/hud/parts/unlock_notification.js index 305a4813..851412e3 100644 --- a/src/js/game/hud/parts/unlock_notification.js +++ b/src/js/game/hud/parts/unlock_notification.js @@ -27,7 +27,7 @@ export class HUDUnlockNotification extends BaseHUDPart { } shouldPauseGame() { - return !G_IS_STANDALONE && this.visible; + return false; } createElements(parent) { diff --git a/src/js/globals.d.ts b/src/js/globals.d.ts index 41c2db95..13e9f3cf 100644 --- a/src/js/globals.d.ts +++ b/src/js/globals.d.ts @@ -1,10 +1,13 @@ // Globals defined by webpack 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( - condition: boolean | object | string, - ...errorMessage: string[] + condition: boolean | object | string, + ...errorMessage: string[] ): asserts condition; declare const abstract: void; @@ -12,8 +15,6 @@ declare const abstract: void; declare const G_APP_ENVIRONMENT: string; declare const G_HAVE_ASSERT: boolean; 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_VERSION: string; @@ -26,146 +27,146 @@ declare const ipcRenderer: any; // Polyfills declare interface String { - replaceAll(search: string, replacement: string): string; + replaceAll(search: string, replacement: string): string; } declare interface CanvasRenderingContext2D { - beginRoundedRect(x: number, y: number, w: number, h: number, r: number): void; - beginCircle(x: number, y: number, r: number): void; + beginRoundedRect(x: number, y: number, w: number, h: number, r: number): void; + beginCircle(x: number, y: number, r: number): void; - msImageSmoothingEnabled: boolean; - mozImageSmoothingEnabled: boolean; - webkitImageSmoothingEnabled: boolean; + msImageSmoothingEnabled: boolean; + mozImageSmoothingEnabled: boolean; + webkitImageSmoothingEnabled: boolean; } // Just for compatibility with the shared code declare interface Logger { - log(...args); - warn(...args); - info(...args); - error(...args); + log(...args); + warn(...args); + info(...args); + error(...args); } declare interface MobileAccessibility { - usePreferredTextZoom(boolean); + usePreferredTextZoom(boolean); } declare interface Window { - // Debugging - activeClickDetectors: Array; + // Debugging + activeClickDetectors: Array; - // Mods - $shapez_registerMod: any; - anyModLoaded: any; + // Mods + $shapez_registerMod: 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 { - app: any; - device: any; - splashscreen: any; + app: any; + device: any; + splashscreen: any; } // Webpack declare interface WebpackContext { - keys(): Array; + keys(): Array; } declare interface NodeRequire { - context(src: string, flag: boolean, regexp: RegExp): WebpackContext; + context(src: string, flag: boolean, regexp: RegExp): WebpackContext; } declare interface Object { - entries(obj: object): Array<[string, any]>; + entries(obj: object): Array<[string, any]>; } declare interface Math { - radians(number): number; - degrees(number): number; + radians(number): number; + degrees(number): number; } declare type Class = new (...args: any[]) => T; declare interface String { - padStart(size: number, fill?: string): string; - padEnd(size: number, fill: string): string; + padStart(size: number, fill?: string): string; + padEnd(size: number, fill: string): string; } declare interface SignalTemplate0 { - add(receiver: () => string | void, scope: null | any); - dispatch(): string | void; - remove(receiver: () => string | void); - removeAll(); + add(receiver: () => string | void, scope: null | any); + dispatch(): string | void; + remove(receiver: () => string | void); + removeAll(); } declare class TypedTrackedState { - 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; - get(): T; + setSilent(value: any): void; + get(): T; } declare type Layer = "regular" | "wires"; declare type ItemType = "shape" | "color" | "boolean"; declare module "worker-loader?inline=true&fallback=false!*" { - class WebpackWorker extends Worker { - constructor(); - } + class WebpackWorker extends Worker { + constructor(); + } - export default WebpackWorker; + export default WebpackWorker; } // JSX type support - https://www.typescriptlang.org/docs/handbook/jsx.html // modified from https://stackoverflow.com/a/68238924 declare namespace JSX { - /** - * The return type of a JSX expression. - * - * In reality, Fragments can return arbitrary values, but we ignore this for convenience. - */ - type Element = HTMLElement; - /** - * 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. - */ - type IntrinsicElements = { - [K in keyof HTMLElementTagNameMap]: { - children?: Node | Node[]; - [k: string]: Node | Node[] | string | number | boolean; - }; + /** + * The return type of a JSX expression. + * + * In reality, Fragments can return arbitrary values, but we ignore this for convenience. + */ + type Element = HTMLElement; + /** + * 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. + */ + type IntrinsicElements = { + [K in keyof HTMLElementTagNameMap]: { + children?: Node | Node[]; + [k: string]: Node | Node[] | string | number | boolean; }; - /** - * The property of the attributes object storing the children. - */ - type ElementChildrenAttribute = { children: unknown }; + }; + /** + * The property of the attributes object storing the children. + */ + 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. - */ - type Props = { [k: string]: unknown }; - /** - * A functional component requiring attributes to match `T`. - */ - type Component = { - (props: T): Element; - }; - /** - * A child of a JSX element. - */ - type Node = Element | string | boolean | null | undefined; + /** + * An attributes object. + */ + type Props = { [k: string]: unknown }; + /** + * A functional component requiring attributes to match `T`. + */ + type Component = { + (props: T): Element; + }; + /** + * A child of a JSX element. + */ + type Node = Element | string | boolean | null | undefined; } diff --git a/src/js/main.js b/src/js/main.js index 17c971ee..acade03b 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -63,8 +63,4 @@ function bootApp() { app.boot(); } -if (G_IS_STANDALONE) { - window.addEventListener("load", bootApp); -} else { - bootApp(); -} +window.addEventListener("load", bootApp); diff --git a/src/js/mods/modloader.js b/src/js/mods/modloader.js index 3104bca3..b5d375a5 100644 --- a/src/js/mods/modloader.js +++ b/src/js/mods/modloader.js @@ -105,7 +105,7 @@ export class ModLoader { } exposeExports() { - if (G_IS_DEV || G_IS_STANDALONE) { + if (G_IS_DEV) { let exports = {}; const modules = import.meta.webpackContext("../", { recursive: true, @@ -140,24 +140,14 @@ export class ModLoader { } async initMods() { - if (!G_IS_STANDALONE && !G_IS_DEV) { - this.initialized = true; - return; - } - // Create a storage for reading mod settings - const storage = G_IS_STANDALONE - ? new StorageImplElectron(this.app) - : new StorageImplBrowserIndexedDB(this.app); + const storage = new StorageImplElectron(this.app); await storage.initialize(); LOG.log("hook:init", this.app, this.app.storage); this.exposeExports(); - let mods = []; - if (G_IS_STANDALONE) { - mods = await ipcRenderer.invoke("get-mods"); - } + let mods = await ipcRenderer.invoke("get-mods"); if (G_IS_DEV && globalConfig.debug.externalModUrl) { const modURLs = Array.isArray(globalConfig.debug.externalModUrl) ? globalConfig.debug.externalModUrl diff --git a/src/js/platform/electron/steam_achievement_provider.js b/src/js/platform/electron/steam_achievement_provider.js index 291bd926..ae470f9c 100644 --- a/src/js/platform/electron/steam_achievement_provider.js +++ b/src/js/platform/electron/steam_achievement_provider.js @@ -99,11 +99,6 @@ export class SteamAchievementProvider extends AchievementProviderInterface { /** @returns {Promise} */ 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; diff --git a/src/js/platform/sound.js b/src/js/platform/sound.js index 28274111..8f8fae87 100644 --- a/src/js/platform/sound.js +++ b/src/js/platform/sound.js @@ -33,16 +33,12 @@ export const SOUNDS = { export const MUSIC = { // The theme always depends on the standalone only, even if running the full // 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 { constructor(key, url) { diff --git a/src/js/profile/application_settings.js b/src/js/profile/application_settings.js index e31dab9e..b454c163 100644 --- a/src/js/profile/application_settings.js +++ b/src/js/profile/application_settings.js @@ -189,7 +189,7 @@ function initializeSettings() { }, /** * @param {Application} app - */ app => G_IS_STANDALONE + */ app => true ), new BoolSetting( @@ -288,7 +288,7 @@ function initializeSettings() { class SettingsStorage { constructor() { this.uiScale = "regular"; - this.fullscreen = G_IS_STANDALONE; + this.fullscreen = true; this.soundVolume = 1.0; this.musicVolume = 1.0; diff --git a/src/js/states/mods.js b/src/js/states/mods.js index 80bcb4b5..2cb41109 100644 --- a/src/js/states/mods.js +++ b/src/js/states/mods.js @@ -13,7 +13,7 @@ export class ModsState extends TextualGameState { } get modsSupported() { - return G_IS_STANDALONE || G_IS_DEV; + return true; } internalGetFullHtml() { @@ -23,15 +23,11 @@ export class ModsState extends TextualGameState {
${ - this.modsSupported && MODS.mods.length > 0 + MODS.mods.length > 0 ? `` : "" } - ${ - this.modsSupported - ? `` - : "" - } +
`; @@ -45,18 +41,6 @@ export class ModsState extends TextualGameState { } getMainContentHTML() { - if (!this.modsSupported) { - return ` -
- -

${T.mods.noModSupport}

-
- - -
- `; - } - if (MODS.mods.length === 0) { return ` @@ -121,10 +105,6 @@ export class ModsState extends TextualGameState { } openModsFolder() { - if (!G_IS_STANDALONE) { - this.dialogs.showWarning(T.global.error, T.mods.folderOnlyStandalone); - return; - } ipcRenderer.invoke("open-mods-folder"); } diff --git a/src/js/states/preload.js b/src/js/states/preload.js index 5b170dfd..37144612 100644 --- a/src/js/states/preload.js +++ b/src/js/states/preload.js @@ -172,10 +172,6 @@ export class PreloadState extends GameState { return; } - if (!G_IS_STANDALONE) { - return; - } - return this.app.storage .readFileAsync("lastversion.bin") .catch(err => { From 894ceab8547c5c9ed35cc6fd3d70d3b473c7eaf1 Mon Sep 17 00:00:00 2001 From: cody-ferguson Date: Wed, 19 Jun 2024 17:12:08 -0500 Subject: [PATCH 2/3] Remove folders and steam stuff --- README.md | 2 +- electron/index.js | 10 +- electron/steam.js | 112 ---------- electron/steam_appid.txt | 1 - gulp/build_variants.js | 2 +- gulp/tasks.js | 2 +- package.json | 16 +- src/js/application.js | 13 +- src/js/globals.d.ts | 17 ++ src/js/mods/modloader.js | 5 +- src/js/platform/browser/sound.js | 207 ------------------ src/js/platform/browser/storage.js | 93 -------- src/js/platform/browser/storage_indexed_db.js | 154 ------------- src/js/platform/browser/wrapper.js | 102 --------- .../electron/steam_achievement_provider.js | 135 ------------ src/js/platform/electron/storage.js | 41 ---- src/js/platform/electron/wrapper.js | 98 --------- .../{browser => }/no_achievement_provider.js | 2 +- src/js/platform/sound.js | 201 +++++++++++++++++ src/js/platform/storage.js | 35 ++- src/js/platform/wrapper.js | 205 ++++++++++------- src/js/states/main_menu.js | 1 - src/js/states/preload.js | 22 -- src/tsconfig.json | 19 +- 24 files changed, 398 insertions(+), 1097 deletions(-) delete mode 100644 electron/steam.js delete mode 100644 electron/steam_appid.txt delete mode 100644 src/js/platform/browser/sound.js delete mode 100644 src/js/platform/browser/storage.js delete mode 100644 src/js/platform/browser/storage_indexed_db.js delete mode 100644 src/js/platform/browser/wrapper.js delete mode 100644 src/js/platform/electron/steam_achievement_provider.js delete mode 100644 src/js/platform/electron/storage.js delete mode 100644 src/js/platform/electron/wrapper.js rename src/js/platform/{browser => }/no_achievement_provider.js (84%) diff --git a/README.md b/README.md index f7eb1409..eea0bef3 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ and does not intend to provide compatibility for older clients. - In the root folder, run `yarn package-$PLATFORM-$ARCH` where: - `$PLATFORM` is `win32`, `linux` or `darwin` depending on your system. - `$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 diff --git a/electron/index.js b/electron/index.js index 4ba296b8..13832bda 100644 --- a/electron/index.js +++ b/electron/index.js @@ -2,7 +2,6 @@ const { app, BrowserWindow, Menu, MenuItem, ipcMain, shell, dialog, session } = const path = require("path"); const url = require("url"); const fs = require("fs"); -const steam = require("./steam"); const asyncLock = require("async-lock"); const windowStateKeeper = require("electron-window-state"); @@ -153,7 +152,7 @@ function createWindow() { win.webContents.on("new-window", (event, pth) => { event.preventDefault(); - if (pth.startsWith("https://") || pth.startsWith("steam://")) { + if (pth.startsWith("https://")) { shell.openExternal(pth); } }); @@ -378,10 +377,3 @@ try { ipcMain.handle("get-mods", async () => { return mods; }); - -steam.init(isDev); - -// Only allow achievements and puzzle DLC if no mods are loaded -if (mods.length === 0) { - steam.listen(); -} diff --git a/electron/steam.js b/electron/steam.js deleted file mode 100644 index cdda540b..00000000 --- a/electron/steam.js +++ /dev/null @@ -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, -}; diff --git a/electron/steam_appid.txt b/electron/steam_appid.txt deleted file mode 100644 index a8e9e809..00000000 --- a/electron/steam_appid.txt +++ /dev/null @@ -1 +0,0 @@ -1318690 diff --git a/gulp/build_variants.js b/gulp/build_variants.js index 54ec0ab7..6a80c2d9 100644 --- a/gulp/build_variants.js +++ b/gulp/build_variants.js @@ -19,7 +19,7 @@ export const BUILD_VARIANTS = { standalone: false, environment: "prod", }, - "standalone-steam": { + "standalone": { standalone: true, executableName: "shapez", }, diff --git a/gulp/tasks.js b/gulp/tasks.js index 8dd5af9d..c612de01 100644 --- a/gulp/tasks.js +++ b/gulp/tasks.js @@ -286,4 +286,4 @@ export const main = { }; // Default task (dev, localhost) -export default gulp.series(serve["standalone-steam"]); +export default gulp.series(serve["standalone"]); diff --git a/package.json b/package.json index ddec7986..266c851e 100644 --- a/package.json +++ b/package.json @@ -12,13 +12,13 @@ "lint": "(eslint . && tsc && tsc -p src) || (tsc && tsc -p src) || tsc -p src", "prettier-all": "prettier --write src/**/*.* && prettier --write gulp/**/*.*", "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-arm64": "gulp --cwd gulp package.standalone-steam.win32-arm64", - "package-linux-x64": "gulp --cwd gulp package.standalone-steam.linux-x64", - "package-linux-arm64": "gulp --cwd gulp package.standalone-steam.linux-arm64", - "package-darwin-x64": "gulp --cwd gulp package.standalone-steam.darwin-x64", - "package-darwin-arm64": "gulp --cwd gulp package.standalone-steam.darwin-arm64", - "package-all": "gulp --cwd gulp package.standalone-steam.all" + "package-win32-x64": "gulp --cwd gulp package.standalone.win32-x64", + "package-win32-arm64": "gulp --cwd gulp package.standalone.win32-arm64", + "package-linux-x64": "gulp --cwd gulp package.standalone.linux-x64", + "package-linux-arm64": "gulp --cwd gulp package.standalone.linux-arm64", + "package-darwin-x64": "gulp --cwd gulp package.standalone.darwin-x64", + "package-darwin-arm64": "gulp --cwd gulp package.standalone.darwin-arm64", + "package-all": "gulp --cwd gulp package.standalone.all" }, "dependencies": { "ajv": "^6.10.2", @@ -95,4 +95,4 @@ "yaml": "^1.10.0", "yarn": "^1.22.4" } -} +} \ No newline at end of file diff --git a/src/js/application.js b/src/js/application.js index 90b197b5..52337c03 100644 --- a/src/js/application.js +++ b/src/js/application.js @@ -10,9 +10,10 @@ import { StateManager } from "./core/state_manager"; import { TrackedState } from "./core/tracked_state"; import { getPlatformName, waitNextFrame } from "./core/utils"; import { Vector } from "./core/vector"; -import { NoAchievementProvider } from "./platform/browser/no_achievement_provider"; -import { SoundImplBrowser } from "./platform/browser/sound"; -import { PlatformWrapperImplElectron } from "./platform/electron/wrapper"; +import { NoAchievementProvider } from "./platform/no_achievement_provider"; +import { Sound } from "./platform/sound"; +import { Storage } from "./platform/storage"; +import { PlatformWrapperImplElectron } from "./platform/wrapper"; import { ApplicationSettings } from "./profile/application_settings"; import { SavegameManager } from "./savegame/savegame_manager"; import { AboutState } from "./states/about"; @@ -33,7 +34,6 @@ import { ModsState } from "./states/mods"; /** * @typedef {import("./platform/achievement_provider").AchievementProviderInterface} AchievementProviderInterface * @typedef {import("./platform/sound").SoundInterface} SoundInterface - * @typedef {import("./platform/storage").StorageInterface} StorageInterface */ const logger = createLogger("application"); @@ -87,12 +87,11 @@ export class Application { // Platform dependent stuff - /** @type {StorageInterface} */ - this.storage = null; + this.storage = new Storage(this); this.platformWrapper = new PlatformWrapperImplElectron(this); - this.sound = new SoundImplBrowser(this); + this.sound = new Sound(this); this.achievementProvider = new NoAchievementProvider(this); // Track if the window is focused (only relevant for browser) diff --git a/src/js/globals.d.ts b/src/js/globals.d.ts index 13e9f3cf..bfbff078 100644 --- a/src/js/globals.d.ts +++ b/src/js/globals.d.ts @@ -30,6 +30,23 @@ declare interface 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 { beginRoundedRect(x: number, y: number, w: number, h: number, r: number): void; beginCircle(x: number, y: number, r: number): void; diff --git a/src/js/mods/modloader.js b/src/js/mods/modloader.js index b5d375a5..41240255 100644 --- a/src/js/mods/modloader.js +++ b/src/js/mods/modloader.js @@ -3,8 +3,7 @@ import { Application } from "../application"; /* typehints:end */ import { globalConfig } from "../core/config"; import { createLogger } from "../core/logging"; -import { StorageImplBrowserIndexedDB } from "../platform/browser/storage_indexed_db"; -import { StorageImplElectron } from "../platform/electron/storage"; +import { Storage } from "../platform/storage"; import { FILE_NOT_FOUND } from "../platform/storage"; import { Mod } from "./mod"; import { ModInterface } from "./mod_interface"; @@ -141,7 +140,7 @@ export class ModLoader { async initMods() { // Create a storage for reading mod settings - const storage = new StorageImplElectron(this.app); + const storage = new Storage(this.app); await storage.initialize(); LOG.log("hook:init", this.app, this.app.storage); diff --git a/src/js/platform/browser/sound.js b/src/js/platform/browser/sound.js deleted file mode 100644 index 0beb0a35..00000000 --- a/src/js/platform/browser/sound.js +++ /dev/null @@ -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} */ - 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()); - } -} diff --git a/src/js/platform/browser/storage.js b/src/js/platform/browser/storage.js deleted file mode 100644 index ac0fa4ca..00000000 --- a/src/js/platform/browser/storage.js +++ /dev/null @@ -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); - }); - } -} diff --git a/src/js/platform/browser/storage_indexed_db.js b/src/js/platform/browser/storage_indexed_db.js deleted file mode 100644 index 1028eed3..00000000 --- a/src/js/platform/browser/storage_indexed_db.js +++ /dev/null @@ -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); - }); - } -} diff --git a/src/js/platform/browser/wrapper.js b/src/js/platform/browser/wrapper.js deleted file mode 100644 index cebb1ed9..00000000 --- a/src/js/platform/browser/wrapper.js +++ /dev/null @@ -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 - } -} diff --git a/src/js/platform/electron/steam_achievement_provider.js b/src/js/platform/electron/steam_achievement_provider.js deleted file mode 100644 index ae470f9c..00000000 --- a/src/js/platform/electron/steam_achievement_provider.js +++ /dev/null @@ -1,135 +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} - */ - 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} */ - initialize() { - 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} - */ - 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; - }); - } -} diff --git a/src/js/platform/electron/storage.js b/src/js/platform/electron/storage.js deleted file mode 100644 index 65f0e507..00000000 --- a/src/js/platform/electron/storage.js +++ /dev/null @@ -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, - }); - } -} diff --git a/src/js/platform/electron/wrapper.js b/src/js/platform/electron/wrapper.js deleted file mode 100644 index c510485f..00000000 --- a/src/js/platform/electron/wrapper.js +++ /dev/null @@ -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"); - } -} diff --git a/src/js/platform/browser/no_achievement_provider.js b/src/js/platform/no_achievement_provider.js similarity index 84% rename from src/js/platform/browser/no_achievement_provider.js rename to src/js/platform/no_achievement_provider.js index 8a8a343e..66a83874 100644 --- a/src/js/platform/browser/no_achievement_provider.js +++ b/src/js/platform/no_achievement_provider.js @@ -1,4 +1,4 @@ -import { AchievementProviderInterface } from "../achievement_provider"; +import { AchievementProviderInterface } from "./achievement_provider"; export class NoAchievementProvider extends AchievementProviderInterface { hasAchievements() { diff --git a/src/js/platform/sound.js b/src/js/platform/sound.js index 8f8fae87..d9cc0afc 100644 --- a/src/js/platform/sound.js +++ b/src/js/platform/sound.js @@ -7,6 +7,10 @@ import { GameRoot } from "../game/root"; import { newEmptyMap, clamp } from "../core/utils"; import { createLogger } from "../core/logging"; import { globalConfig } from "../core/config"; +import { Howl, Howler } from "howler"; + +// @ts-ignore +import sprites from "../built-temp/sfx.json"; const logger = createLogger("sound"); @@ -288,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} */ + 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()); + } +} diff --git a/src/js/platform/storage.js b/src/js/platform/storage.js index c5c3701c..ac2be5be 100644 --- a/src/js/platform/storage.js +++ b/src/js/platform/storage.js @@ -4,7 +4,7 @@ import { Application } from "../application"; export const FILE_NOT_FOUND = "file_not_found"; -export class StorageInterface { +export class Storage { constructor(app) { /** @type {Application} */ this.app = app; @@ -13,11 +13,9 @@ export class StorageInterface { /** * Initializes the storage * @returns {Promise} - * @abstract */ initialize() { - abstract; - return Promise.reject(); + return Promise.resolve(); } /** @@ -25,22 +23,33 @@ export class StorageInterface { * @param {string} filename * @param {string} contents * @returns {Promise} - * @abstract */ writeFileAsync(filename, contents) { - abstract; - return Promise.reject(); + return ipcRenderer.invoke("fs-job", { + type: "write", + filename, + contents, + }); } /** * Reads a string asynchronously. Returns Promise if file was not found. * @param {string} filename * @returns {Promise} - * @abstract */ readFileAsync(filename) { - abstract; - return Promise.reject(); + return ipcRenderer + .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} */ deleteFileAsync(filename) { - // Default implementation does not allow deleting files - return Promise.reject(); + return ipcRenderer.invoke("fs-job", { + type: "delete", + filename, + }); } } diff --git a/src/js/platform/wrapper.js b/src/js/platform/wrapper.js index 0db580d2..fac58541 100644 --- a/src/js/platform/wrapper.js +++ b/src/js/platform/wrapper.js @@ -3,30 +3,60 @@ import { Application } from "../application"; /* typehints:end */ 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) { /** @type {Application} */ 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() { - abstract; - return "unknown-platform"; + return "electron"; + } + + getSupportsRestart() { + return true; } /** - * Returns the UI scale, called on every resize - * @returns {number} */ - getUiScale() { - return 1; - } - - /** @returns {boolean} */ - getSupportsRestart() { - abstract; - return false; + * Attempt to open an external url + * @param {string} url + */ + openExternalLink(url) { + logger.log(this, "Opening external:", url); + window.open(url, "about:blank"); } /** @@ -36,10 +66,86 @@ export class PlatformWrapperInterface { return 1; } - /** @returns {Promise} */ - initialize() { - document.documentElement.classList.add("p-" + this.getId()); - return Promise.resolve(); + /** + * Should return if this platform supports ads at all + */ + 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() { 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; - } } diff --git a/src/js/states/main_menu.js b/src/js/states/main_menu.js index 14c23577..0ec4dcab 100644 --- a/src/js/states/main_menu.js +++ b/src/js/states/main_menu.js @@ -16,7 +16,6 @@ import { } from "../core/utils"; import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs"; import { MODS } from "../mods/modloader"; -import { PlatformWrapperImplElectron } from "../platform/electron/wrapper"; import { Savegame } from "../savegame/savegame"; import { T } from "../translations"; diff --git a/src/js/states/preload.js b/src/js/states/preload.js index 37144612..872477f0 100644 --- a/src/js/states/preload.js +++ b/src/js/states/preload.js @@ -5,7 +5,6 @@ import { createLogger } from "../core/logging"; import { getLogoSprite, timeoutPromise } from "../core/utils"; import { getRandomHint } from "../game/hints"; import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs"; -import { PlatformWrapperImplBrowser } from "../platform/browser/wrapper"; import { autoDetectLanguageId, T, updateApplicationLanguage } from "../translations"; const logger = createLogger("state/preload"); @@ -74,27 +73,6 @@ export class PreloadState extends GameState { .then(() => this.setStatus("Creating platform wrapper", 3)) .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(() => { return this.app.storage.initialize(); diff --git a/src/tsconfig.json b/src/tsconfig.json index aad7a9f4..6a61addc 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -1,19 +1,24 @@ { - "extends": ["@tsconfig/strictest/tsconfig"], - "include": ["./js/**/*"], + "extends": [ + "@tsconfig/strictest/tsconfig" + ], + "include": [ + "./js/**/*" + ], "compilerOptions": { "allowJs": true, "module": "es2022", "moduleResolution": "bundler", "noEmit": true, - + "target": "ES2022", /* JSX Compilation */ "paths": { - "@/*": ["./js/*"] + "@/*": [ + "./js/*" + ] }, "jsx": "react-jsx", "jsxImportSource": "@", - // remove when comfortable "exactOptionalPropertyTypes": false, "noImplicitAny": false, @@ -23,6 +28,6 @@ "noUncheckedIndexedAccess": false, "strictNullChecks": false, // eslint warns for this - "noUnusedLocals": true + "noUnusedLocals": true, } -} +} \ No newline at end of file From 168e0bcb6dc4197ea8a529b367b20fdc695d03c3 Mon Sep 17 00:00:00 2001 From: cody-ferguson <92231243+cody-ferguson@users.noreply.github.com> Date: Wed, 19 Jun 2024 18:05:46 -0500 Subject: [PATCH 3/3] Fix import assertion --- gulp/standalone.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gulp/standalone.js b/gulp/standalone.js index cdcc883d..40f9452d 100644 --- a/gulp/standalone.js +++ b/gulp/standalone.js @@ -1,5 +1,5 @@ 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 { getVersion } from "./buildutils.js"; import fs from "fs/promises";